Repository: evcc-io/evcc
Pull request #6568 (2023-03-01 22:00:55)

Consistent soc formatting in battery tooltip
Fix https://github.com/evcc-io/evcc/issues/6566

- always round battery soc
- move soc% formatting from translations to code (ensures consistent spacing, see #6564)

naltatis: Hint: translation files changed. We might need to lock/unlock weblate to avoid conflicts when merging this into master.

Patch:
diff --git a/api/actionconfig.go b/api/actionconfig.go
index 7cc6d161..c5d37ced 100644
--- a/api/actionconfig.go
+++ b/api/actionconfig.go
@@ -6,6 +6,7 @@ import (
 	"strings"
 
 	"github.com/fatih/structs"
+	"github.com/imdario/mergo"
 )
 
 // ActionConfig defines an action to take on event
@@ -21,20 +22,8 @@ type ActionConfig struct {
 // Merge merges all non-nil properties of the additional config into the base config.
 // The receiver's config remains immutable.
 func (a ActionConfig) Merge(m ActionConfig) ActionConfig {
-	if m.Mode != nil {
-		a.Mode = m.Mode
-	}
-	if m.MinCurrent != nil {
-		a.MinCurrent = m.MinCurrent
-	}
-	if m.MaxCurrent != nil {
-		a.MaxCurrent = m.MaxCurrent
-	}
-	if m.MinSoc != nil {
-		a.MinSoc = m.MinSoc
-	}
-	if m.TargetSoc != nil {
-		a.TargetSoc = m.TargetSoc
+	if err := mergo.MergeWithOverwrite(&a, m); err != nil {
+		panic(err)
 	}
 	return a
 }
diff --git a/assets/js/components/Energyflow/Energyflow.story.vue b/assets/js/components/Energyflow/Energyflow.story.vue
index 5d436d6b..047c6ed3 100644
--- a/assets/js/components/Energyflow/Energyflow.story.vue
+++ b/assets/js/components/Energyflow/Energyflow.story.vue
@@ -29,8 +29,8 @@ import Energyflow from "./Energyflow.vue";
 				:batterySoc="77"
 				siteTitle="Home"
 				:battery="[
-					{ soc: 44, capacity: 13.3 },
-					{ soc: 82, capacity: 21 },
+					{ soc: 44.999, capacity: 13.3 },
+					{ soc: 82.3331, capacity: 21 },
 				]"
 			/>
 		</Variant>
diff --git a/assets/js/components/Energyflow/Energyflow.vue b/assets/js/components/Energyflow/Energyflow.vue
index 2d776335..e1985984 100644
--- a/assets/js/components/Energyflow/Energyflow.vue
+++ b/assets/js/components/Energyflow/Energyflow.vue
@@ -232,7 +232,12 @@ export default {
 			return this.battery.map(({ soc, capacity }) => {
 				const energy = this.fmtKWh((capacity / 100) * soc * 1e3, true, false, 1);
 				const total = this.fmtKWh(capacity * 1e3, true, true, 1);
-				return this.$t("main.energyflow.batteryTooltip", { energy, total, soc });
+				const formattedSoc = this.batteryFmt(soc);
+				return this.$t("main.energyflow.batteryTooltip", {
+					energy,
+					total,
+					soc: formattedSoc,
+				});
 			});
 		},
 		batteryFmt() {
diff --git a/charger/fritzdect.go b/charger/fritzdect.go
index 7ad6468f..3141b3ea 100644
--- a/charger/fritzdect.go
+++ b/charger/fritzdect.go
@@ -34,6 +34,10 @@ func NewFritzDECTFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
+	if cc.User == "" || cc.Password == "" {
+		return nil, api.ErrMissingCredentials
+	}
+
 	return NewFritzDECT(cc.embed, cc.URI, cc.AIN, cc.User, cc.Password, cc.StandbyPower)
 }
 
@@ -104,6 +108,7 @@ func (c *FritzDECT) MaxCurrent(current int64) error {
 	return nil
 }
 
+var _ api.Meter = (*FritzDECT)(nil)
 var _ api.MeterEnergy = (*FritzDECT)(nil)
 
 // TotalEnergy implements the api.MeterEnergy interface
diff --git a/charger/homematic.go b/charger/homematic.go
index efd57c90..700c8b8b 100644
--- a/charger/homematic.go
+++ b/charger/homematic.go
@@ -33,6 +33,10 @@ func NewCCUFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
+	if cc.User == "" || cc.Password == "" {
+		return nil, api.ErrMissingCredentials
+	}
+
 	return NewCCU(cc.embed, cc.URI, cc.Device, cc.MeterChannel, cc.SwitchChannel, cc.User, cc.Password, cc.StandbyPower)
 }
 
@@ -64,6 +68,7 @@ func (c *CCU) MaxCurrent(current int64) error {
 	return nil
 }
 
+var _ api.Meter = (*CCU)(nil)
 var _ api.MeterEnergy = (*CCU)(nil)
 
 // TotalEnergy implements the api.MeterEnergy interface
diff --git a/charger/mystrom.go b/charger/mystrom.go
index 3edd4fc3..3115a997 100644
--- a/charger/mystrom.go
+++ b/charger/mystrom.go
@@ -67,3 +67,5 @@ func (c *MyStrom) Enable(enable bool) error {
 func (c *MyStrom) MaxCurrent(current int64) error {
 	return nil
 }
+
+var _ api.Meter = (*MyStrom)(nil)
diff --git a/charger/shelly.go b/charger/shelly.go
index 53b2a904..440d37cb 100644
--- a/charger/shelly.go
+++ b/charger/shelly.go
@@ -81,6 +81,7 @@ func (c *Shelly) MaxCurrent(current int64) error {
 	return nil
 }
 
+var _ api.Meter = (*Shelly)(nil)
 var _ api.MeterEnergy = (*Shelly)(nil)
 
 // TotalEnergy implements the api.MeterEnergy interface
diff --git a/charger/tapo.go b/charger/tapo.go
index 533a7139..3fdc2ec8 100644
--- a/charger/tapo.go
+++ b/charger/tapo.go
@@ -30,6 +30,10 @@ func NewTapoFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
+	if cc.User == "" || cc.Password == "" {
+		return nil, api.ErrMissingCredentials
+	}
+
 	return NewTapo(cc.embed, cc.URI, cc.User, cc.Password, cc.StandbyPower)
 }
 
@@ -69,6 +73,7 @@ func (c *Tapo) MaxCurrent(current int64) error {
 	return nil
 }
 
+var _ api.Meter = (*Tapo)(nil)
 var _ api.ChargeRater = (*Tapo)(nil)
 
 // ChargedEnergy implements the api.ChargeRater interface
diff --git a/charger/tasmota.go b/charger/tasmota.go
index 5cfc3c2d..387ac83c 100644
--- a/charger/tasmota.go
+++ b/charger/tasmota.go
@@ -173,6 +173,7 @@ func (c *Tasmota) MaxCurrent(current int64) error {
 	return nil
 }
 
+var _ api.Meter = (*Tasmota)(nil)
 var _ api.MeterEnergy = (*Tasmota)(nil)
 
 // TotalEnergy implements the api.MeterEnergy interface
diff --git a/charger/tplink.go b/charger/tplink.go
index dc6164ad..47a79b65 100644
--- a/charger/tplink.go
+++ b/charger/tplink.go
@@ -97,6 +97,7 @@ func (c *TPLink) MaxCurrent(current int64) error {
 	return nil
 }
 
+var _ api.Meter = (*TPLink)(nil)
 var _ api.MeterEnergy = (*TPLink)(nil)
 
 // TotalEnergy implements the api.MeterEnergy interface
diff --git a/charger/twc3.go b/charger/twc3.go
index 4d814cf9..af9fb7e4 100644
--- a/charger/twc3.go
+++ b/charger/twc3.go
@@ -169,6 +169,22 @@ func (v *Twc3) Currents() (float64, float64, float64, error) {
 	return res.CurrentAA, res.CurrentBA, res.CurrentCA, err
 }
 
+var _ api.Meter = (*Twc3)(nil)
+
+// CurrentPower implements the api.Meter interface
+func (v *Twc3) CurrentPower() (float64, error) {
+	res, err := v.vitalsG()
+	return (res.CurrentAA + res.CurrentBA + res.CurrentCA) * res.GridV, err
+}
+
+var _ api.PhaseVoltages = (*Twc3)(nil)
+
+// Voltages implements the api.PhaseVoltages interface
+func (v *Twc3) Voltages() (float64, float64, float64, error) {
+	res, err := v.vitalsG()
+	return res.VoltageAV, res.VoltageBV, res.VoltageCV, err
+}
+
 var _ loadpoint.Controller = (*Twc3)(nil)
 
 // LoadpointControl implements loadpoint.Controller
diff --git a/cmd/dumper.go b/cmd/dumper.go
index d51d8850..139a9d53 100644
--- a/cmd/dumper.go
+++ b/cmd/dumper.go
@@ -212,6 +212,13 @@ func (d *dumper) Dump(name string, v interface{}) {
 		}
 	}
 
+	// features
+
+	if v, ok := v.(api.FeatureDescriber); ok {
+		ff := v.Features()
+		fmt.Fprintf(w, "Features:\t%v\n", ff)
+	}
+
 	w.Flush()
 }
 
diff --git a/go.mod b/go.mod
index 2f1eaf41..90aac227 100644
--- a/go.mod
+++ b/go.mod
@@ -6,12 +6,12 @@ require (
 	github.com/AlecAivazis/survey/v2 v2.3.6
 	github.com/BurntSushi/toml v1.2.1
 	github.com/Masterminds/sprig/v3 v3.2.3
-	github.com/PuerkitoBio/goquery v1.8.0
+	github.com/PuerkitoBio/goquery v1.8.1
 	github.com/andig/gosunspec v0.0.0-20211108155140-af2e73b86e71
 	github.com/andig/mbserver v0.0.0-20221101171752-364e71b0089c
 	github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef
 	github.com/avast/retry-go/v3 v3.1.1
-	github.com/aws/aws-sdk-go v1.44.203
+	github.com/aws/aws-sdk-go v1.44.212
 	github.com/basgys/goxml2json v1.1.0
 	github.com/basvdlei/gotsmart v0.0.3
 	github.com/benbjohnson/clock v1.3.0
@@ -46,19 +46,19 @@ require (
 	github.com/gregdel/pushover v1.1.0
 	github.com/grid-x/modbus v0.0.0-20230105075906-8b31e0d1647e
 	github.com/hashicorp/go-version v1.6.0
-	github.com/hasura/go-graphql-client v0.9.0
+	github.com/hasura/go-graphql-client v0.9.1
 	github.com/imdario/mergo v0.3.13
 	github.com/influxdata/influxdb-client-go/v2 v2.12.2
-	github.com/itchyny/gojq v0.12.11
+	github.com/itchyny/gojq v0.12.12
 	github.com/jeremywohl/flatten v1.0.1
 	github.com/jinzhu/copier v0.3.5
 	github.com/joeshaw/carwings v0.0.0-20220711204617-d90801cf2848
 	github.com/joho/godotenv v1.5.1
 	github.com/jpfielding/go-http-digest v0.0.0-20211006141426-fbc93758452e
 	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
-	github.com/koron/go-ssdp v0.0.3
+	github.com/koron/go-ssdp v0.0.4
 	github.com/korylprince/ipnetgen v1.0.1
-	github.com/kr/pretty v0.3.0
+	github.com/kr/pretty v0.3.1
 	github.com/libp2p/zeroconf/v2 v2.2.0
 	github.com/lorenzodonini/ocpp-go v0.16.1-0.20230224180512-0195c3faf41a
 	github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40
@@ -77,7 +77,7 @@ require (
 	github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
 	github.com/prometheus-community/pro-bing v0.1.0
 	github.com/prometheus/client_golang v1.14.0
-	github.com/prometheus/common v0.39.0
+	github.com/prometheus/common v0.41.0
 	github.com/robertkrimen/otto v0.2.1
 	github.com/samber/lo v1.37.0
 	github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29
@@ -86,21 +86,21 @@ require (
 	github.com/spf13/jwalterweatherman v1.1.0
 	github.com/spf13/pflag v1.0.5
 	github.com/spf13/viper v1.15.0
-	github.com/stretchr/testify v1.8.1
+	github.com/stretchr/testify v1.8.2
 	github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c
-	github.com/volkszaehler/mbmd v0.0.0-20230202102845-c58e07a6f048
+	github.com/volkszaehler/mbmd v0.0.0-20230218103719-dd45a8a8dfe9
 	github.com/writeas/go-strip-markdown v2.0.1+incompatible
 	gitlab.com/bboehmke/sunny v0.15.1-0.20211022160056-2fba1c86ade6
-	golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb
+	golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2
 	golang.org/x/net v0.7.0
 	golang.org/x/oauth2 v0.5.0
 	golang.org/x/sync v0.1.0
 	golang.org/x/text v0.7.0
-	google.golang.org/api v0.110.0
+	google.golang.org/api v0.111.0
 	google.golang.org/grpc v1.53.0
 	google.golang.org/protobuf v1.28.1
 	gopkg.in/yaml.v3 v3.0.1
-	gorm.io/gorm v1.24.5
+	gorm.io/gorm v1.24.6
 )
 
 require (
@@ -144,23 +144,23 @@ require (
 	github.com/jinzhu/now v1.1.5 // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
-	github.com/klauspost/compress v1.15.15 // indirect
+	github.com/klauspost/compress v1.16.0 // indirect
 	github.com/kr/text v0.2.0 // indirect
-	github.com/leodido/go-urn v1.2.1 // indirect
+	github.com/leodido/go-urn v1.2.2 // indirect
 	github.com/magiconair/properties v1.8.7 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.17 // indirect
 	github.com/mattn/go-runewidth v0.0.14 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
 	github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
-	github.com/miekg/dns v1.1.50 // indirect
+	github.com/miekg/dns v1.1.51 // 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
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/onsi/ginkgo v1.16.5 // indirect
 	github.com/pascaldekloe/name v1.0.1 // indirect
-	github.com/pelletier/go-toml/v2 v2.0.6 // indirect
+	github.com/pelletier/go-toml/v2 v2.0.7 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/prometheus/client_model v0.3.0 // indirect
@@ -168,11 +168,11 @@ require (
 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
 	github.com/rickb777/date v1.20.1 // indirect
 	github.com/rickb777/plural v1.4.1 // indirect
-	github.com/rivo/uniseg v0.4.3 // indirect
+	github.com/rivo/uniseg v0.4.4 // indirect
 	github.com/rogpeppe/go-internal v1.9.0 // indirect
 	github.com/shopspring/decimal v1.3.1 // indirect
 	github.com/sirupsen/logrus v1.9.0 // indirect
-	github.com/spf13/afero v1.9.3 // indirect
+	github.com/spf13/afero v1.9.4 // indirect
 	github.com/spf13/cast v1.5.0 // indirect
 	github.com/subosito/gotenv v1.4.2 // indirect
 	github.com/teivah/onecontext v1.3.0 // indirect
@@ -186,14 +186,14 @@ require (
 	golang.org/x/term v0.5.0 // indirect
 	golang.org/x/tools v0.6.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44 // indirect
+	google.golang.org/genproto v0.0.0-20230301171018-9ab4bdc49ad5 // 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.22.2 // indirect
+	modernc.org/libc v1.22.3 // indirect
 	modernc.org/mathutil v1.5.0 // indirect
 	modernc.org/memory v1.5.0 // indirect
-	modernc.org/sqlite v1.20.4 // indirect
+	modernc.org/sqlite v1.21.0 // indirect
 	nhooyr.io/websocket v1.8.7 // indirect
 )
 
diff --git a/go.sum b/go.sum
index 4d7ec11d..1c29e396 100644
--- a/go.sum
+++ b/go.sum
@@ -32,7 +32,7 @@ cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w9
 cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc=
 cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU=
 cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA=
-cloud.google.com/go v0.107.0 h1:qkj22L7bgkl6vIeZDlOY2po43Mx/TIa2Wsa7VR+PEww=
+cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=
 cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw=
 cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY=
 cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI=
@@ -119,7 +119,7 @@ cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx
 cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI=
 cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8=
 cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08=
-cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs=
+cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=
 cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4=
 cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w=
 cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE=
@@ -206,8 +206,8 @@ github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBa
 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/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
-github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
+github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
+github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
 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=
@@ -247,8 +247,8 @@ github.com/avast/retry-go/v3 v3.1.1 h1:49Scxf4v8PmiQ/nY0aY3p0hDueqSmc7++cBbtiDGu
 github.com/avast/retry-go/v3 v3.1.1/go.mod h1:6cXRK369RpzFL3UQGqIUp9Q7GDrams+KsYWrfNA1/nQ=
 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.44.203 h1:pcsP805b9acL3wUqa4JR2vg1k2wnItkDYNvfmcy6F+U=
-github.com/aws/aws-sdk-go v1.44.203/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
+github.com/aws/aws-sdk-go v1.44.212 h1:IRstlErdeKeQ8qBsCwWt4MG2RihUOcUJVqYwbvqpE28=
+github.com/aws/aws-sdk-go v1.44.212/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
 github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
 github.com/basgys/goxml2json v1.1.0 h1:4ln5i4rseYfXNd86lGEB+Vi652IsIXIvggKM/BhUKVw=
 github.com/basgys/goxml2json v1.1.0/go.mod h1:wH7a5Np/Q4QoECFIU8zTQlZwZkrilY0itPfecMw41Dw=
@@ -575,8 +575,8 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
 github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
 github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/graph-gophers/graphql-go v1.4.0 h1:JE9wveRTSXwJyjdRd6bOQ7Ob5bewTUQ58Jv4OiVdpdE=
-github.com/graph-gophers/graphql-go v1.4.0/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os=
+github.com/graph-gophers/graphql-go v1.5.0 h1:fDqblo50TEpD0LY7RXk/LFVYEVqo3+tXMNMPSVXA1yc=
+github.com/graph-gophers/graphql-go v1.5.0/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os=
 github.com/graph-gophers/graphql-transport-ws v0.0.2 h1:DbmSkbIGzj8SvHei6n8Mh9eLQin8PtA8xY9eCzjRpvo=
 github.com/graph-gophers/graphql-transport-ws v0.0.2/go.mod h1:5BVKvFzOd2BalVIBFfnfmHjpJi/MZ5rOj8G55mXvZ8g=
 github.com/gregdel/pushover v1.1.0 h1:dwHyvrcpZCOS9V1fAnKPaGRRI5OC55cVaKhMybqNsKQ=
@@ -637,8 +637,8 @@ github.com/hashicorp/memberlist v0.3.1/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn
 github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
 github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
 github.com/hashicorp/serf v0.9.8/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
-github.com/hasura/go-graphql-client v0.9.0 h1:IPL+8CjLrTw4lAJno6qP6e9Nz64+fHSwJTbx4feXI80=
-github.com/hasura/go-graphql-client v0.9.0/go.mod h1:jlTUQWgTJOI4opQssMlCkzVjpRVSyL+PQsTjkAwcr3w=
+github.com/hasura/go-graphql-client v0.9.1 h1:RBnaMLdK+muvauxcRitn73BzWnYWzWpKQ3n++eG2+wk=
+github.com/hasura/go-graphql-client v0.9.1/go.mod h1:AarJlxO1I59MPqU/TC7gQP0BMFgPEqUTt5LYPvykasw=
 github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
 github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
 github.com/holoplot/go-avahi v1.0.1 h1:XcqR2keL4qWRnlxHD5CAOdWpLFZJ+EOUK0vEuylfvvk=
@@ -662,8 +662,8 @@ github.com/influxdata/influxdb-client-go/v2 v2.12.2/go.mod h1:YteV91FiQxRdccyJ2c
 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/itchyny/gojq v0.12.11 h1:YhLueoHhHiN4mkfM+3AyJV6EPcCxKZsOnYf+aVSwaQw=
-github.com/itchyny/gojq v0.12.11/go.mod h1:o3FT8Gkbg/geT4pLI0tF3hvip5F3Y/uskjRz9OYa38g=
+github.com/itchyny/gojq v0.12.12 h1:x+xGI9BXqKoJQZkr95ibpe3cdrTbY8D9lonrK433rcA=
+github.com/itchyny/gojq v0.12.12/go.mod h1:j+3sVkjxwd7A7Z5jrbKibgOLn0ZfLWkV+Awxr/pyzJE=
 github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
 github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
 github.com/jarcoal/httpmock v1.2.0 h1:gSvTxxFR/MEMfsGrvRbdfpRUMBStovlSRLw0Ep1bwwc=
@@ -674,7 +674,6 @@ github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg=
 github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
 github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
-github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
 github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
@@ -714,12 +713,12 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
-github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
-github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4=
+github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
+github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/koron/go-ssdp v0.0.3 h1:JivLMY45N76b4p/vsWGOKewBQu6uf39y8l+AQ7sDKx8=
-github.com/koron/go-ssdp v0.0.3/go.mod h1:b2MxI6yh02pKrsyNoQUsk4+YNikaGhe4894J+Q5lDvA=
+github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0=
+github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk=
 github.com/korylprince/ipnetgen v1.0.1 h1:FIv2YOg3LBjyiwGV/b9m/U5QEe6qP8yNic4l7AJznSY=
 github.com/korylprince/ipnetgen v1.0.1/go.mod h1:S0y7uqM4hX9g3NstxMbp6qmfHDSSXuEwcp05bb2zaiw=
 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
@@ -727,16 +726,17 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
-github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
 github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
 github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
-github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
-github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
+github.com/leodido/go-urn v1.2.2 h1:7z68G0FCGvDk646jz1AelTYNYWrTNm0bEcFAo147wt4=
+github.com/leodido/go-urn v1.2.2/go.mod h1:kUaIbLZWttglzwNuG0pgsh5vuV6u2YcGBYz1hIPjtOQ=
 github.com/libp2p/zeroconf/v2 v2.2.0 h1:Cup06Jv6u81HLhIj1KasuNM/RHHrJ8T7wOTS4+Tv53Q=
 github.com/libp2p/zeroconf/v2 v2.2.0/go.mod h1:fuJqLnUwZTshS3U/bMRJ3+ow/v9oid1n0DmyYyNO1Xs=
 github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
@@ -788,8 +788,8 @@ github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3N
 github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
 github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
 github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
-github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
-github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
+github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo=
+github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c=
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
 github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
 github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
@@ -888,8 +888,8 @@ github.com/paypal/gatt v0.0.0-20151011220935-4ae819d591cf/go.mod h1:+AwQL2mK3Pd3
 github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
 github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
 github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
-github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
-github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
+github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us=
+github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
 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.1-0.20220829124759-bd5ffb679356 h1:YTrZVN3AD7w5qrpb8etI9mwHjk8dtZBNUU9yhoeXfgs=
@@ -898,6 +898,7 @@ github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0
 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -935,8 +936,8 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b
 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.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
-github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI=
-github.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NXd5w0BbEX0Y=
+github.com/prometheus/common v0.41.0 h1:npo01n6vUlRViIj5fgwiK8vlNIh8bnoxqh3gypKsyAw=
+github.com/prometheus/common v0.41.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
 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=
@@ -954,8 +955,8 @@ github.com/rickb777/date v1.20.1/go.mod h1:9MqjVxT6a/AQTA4nxj9E6G3ksQiMESTn9/9kf
 github.com/rickb777/plural v1.4.1 h1:5MMLcbIaapLFmvDGRT5iPk8877hpTPt8Y9cdSKRw9sU=
 github.com/rickb777/plural v1.4.1/go.mod h1:kdmXUpmKBJTS0FtG/TFumd//VBWsNTD7zOw7x4umxNw=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
-github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw=
-github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
+github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGGF0=
 github.com/robertkrimen/otto v0.2.1/go.mod h1:UPwtJ1Xu7JrLcZjNWN8orJaM5n5YEtqL//farB5FlRY=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
@@ -966,6 +967,7 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV
 github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef/go.mod h1:8AEUvGVi2uQ5b24BIhcr0GCcpd/RNAFWaN2CJFrWIIQ=
 github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 github.com/sagikazarmark/crypt v0.8.0/go.mod h1:TmKwZAo97S4Fy4sfMH/HX/cQP5D+ijra2NyLpNNmttY=
@@ -995,8 +997,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/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
 github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
-github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
-github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
+github.com/spf13/afero v1.9.4 h1:Sd43wM1IWz/s1aVXdOBkjJvuP8UdyqioeE4AmM0QsBs=
+github.com/spf13/afero v1.9.4/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
 github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
 github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
@@ -1029,8 +1031,9 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/suapapa/go_eddystone v1.3.1/go.mod h1:bXC11TfJOS+3g3q/Uzd7FKd5g62STQEfeEIhcKe4Qy8=
 github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
 github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
@@ -1052,8 +1055,8 @@ github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9
 github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
 github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
 github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
-github.com/volkszaehler/mbmd v0.0.0-20230202102845-c58e07a6f048 h1:5H3b7o4SVvfJcMLS4qZDk/LNanxOYwoPZF9811F/4FQ=
-github.com/volkszaehler/mbmd v0.0.0-20230202102845-c58e07a6f048/go.mod h1:y/fJlrURfcBYml7YX+EyLMB7TWFG9IrFGojOSUFPZio=
+github.com/volkszaehler/mbmd v0.0.0-20230218103719-dd45a8a8dfe9 h1:zsQ/O2dQIF4rF3looZEFno98LH4ZZccGd8CbUUC0iM0=
+github.com/volkszaehler/mbmd v0.0.0-20230218103719-dd45a8a8dfe9/go.mod h1:PVEqdIK81Plg4e/b4QaroxpjtwIGo2IK1mTLuoaDbMU=
 github.com/writeas/go-strip-markdown v2.0.1+incompatible h1:IIqxTM5Jr7RzhigcL6FkrCNfXkvbR+Nbu1ls48pXYcw=
 github.com/writeas/go-strip-markdown v2.0.1+incompatible/go.mod h1:Rsyu10ZhbEK9pXdk8V6MVnZmTzRG0alMNLMwa0J01fE=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
@@ -1117,6 +1120,7 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
 golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
@@ -1132,8 +1136,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
-golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb h1:PaBZQdo+iSDyHT053FjUCgZQ/9uqVwPOcl7KSWhKn6w=
-golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
+golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 h1:Jvc7gsqn21cJHCmAWx0LiimpP18LZmUxkT5Mp7EZ1mI=
+golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -1162,6 +1166,7 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
+golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1215,7 +1220,6 @@ golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy
 golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
 golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
 golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@@ -1483,10 +1487,10 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
+golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
 golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1547,8 +1551,8 @@ google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ
 google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
 google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70=
 google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo=
-google.golang.org/api v0.110.0 h1:l+rh0KYUooe9JGbGVx71tbFo4SMbMTXK3I3ia2QSEeU=
-google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI=
+google.golang.org/api v0.111.0 h1:bwKi+z2BsdwYFRKrqwutM+axAlYLz83gt5pDSXCJT+0=
+google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -1662,8 +1666,8 @@ google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqw
 google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM=
 google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM=
 google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s=
-google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44 h1:EfLuoKW5WfkgVdDy7dTK8qSbH37AX5mj/MFh+bGPz14=
-google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA=
+google.golang.org/genproto v0.0.0-20230301171018-9ab4bdc49ad5 h1:/cadn7taPtPlCgiWNetEPsle7jgnlad2R7gR5MXB6dM=
+google.golang.org/genproto v0.0.0-20230301171018-9ab4bdc49ad5/go.mod h1:TvhZT5f700eVlTNwND1xoEZQeWTB2RY/65kplwl/bFA=
 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=
@@ -1763,8 +1767,8 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C
 gopkg.in/yaml.v3 v3.0.0/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.24.5 h1:g6OPREKqqlWq4kh/3MCQbZKImeB9e6Xgc4zD+JgNZGE=
-gorm.io/gorm v1.24.5/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
+gorm.io/gorm v1.24.6 h1:wy98aq9oFEetsc4CAbKD2SoBCdMzsbSIvSUUFJuHi5s=
+gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
 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-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -1773,14 +1777,14 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0=
-modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug=
+modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY=
+modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw=
 modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
 modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
 modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
 modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
-modernc.org/sqlite v1.20.4 h1:J8+m2trkN+KKoE7jglyHYYYiaq5xmz2HoHJIiBlRzbE=
-modernc.org/sqlite v1.20.4/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A=
+modernc.org/sqlite v1.21.0 h1:4aP4MdUf15i3R3M2mx6Q90WHKz3nZLoz96zlB6tNdow=
+modernc.org/sqlite v1.21.0/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI=
 nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
 nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
 pgregory.net/rapid v0.4.7 h1:MTNRktPuv5FNqOO151TM9mDTa+XHcX6ypYeISDVD14g=
diff --git a/i18n/da.toml b/i18n/da.toml
index a171ff19..918688f5 100644
--- a/i18n/da.toml
+++ b/i18n/da.toml
@@ -72,7 +72,7 @@ vehicles = "Parkering"
 battery = "Batteri"
 batteryCharge = "Battery opladning"
 batteryDischarge = "Batteriet aflades"
-batteryTooltip = "{energi} af {total} ({soc}%)"
+batteryTooltip = "{energi} af {total} ({soc})"
 gridImport = "Grid brug"
 homePower = "Forbrug"
 loadpoints = "Oplader| Oplader | {count} opladere"
diff --git a/i18n/de.toml b/i18n/de.toml
index e4437e83..064dd406 100644
--- a/i18n/de.toml
+++ b/i18n/de.toml
@@ -72,7 +72,7 @@ vehicles = "Parkplatz"
 battery = "Batterie"
 batteryCharge = "Batterie laden"
 batteryDischarge = "Batterie entladen"
-batteryTooltip = "{energy} von {total} ({soc}%)"
+batteryTooltip = "{energy} von {total} ({soc})"
 gridImport = "Netzbezug"
 homePower = "Verbrauch"
 loadpoints = "Ladepunkt | Ladepunkt | {count} Ladepunkte"
diff --git a/i18n/en.toml b/i18n/en.toml
index 4d6c44e8..538428e5 100644
--- a/i18n/en.toml
+++ b/i18n/en.toml
@@ -72,7 +72,7 @@ vehicles = "Parking"
 battery = "Battery"
 batteryCharge = "Battery charging"
 batteryDischarge = "Battery discharging"
-batteryTooltip = "{energy} of {total} ({soc}%)"
+batteryTooltip = "{energy} of {total} ({soc})"
 gridImport = "Grid use"
 homePower = "Consumption"
 loadpoints = "Charger| Charger | {count} chargers"
diff --git a/i18n/es.toml b/i18n/es.toml
index d595cbb1..a4175677 100644
--- a/i18n/es.toml
+++ b/i18n/es.toml
@@ -77,7 +77,7 @@ vehicles = "Estacionamiento"
 battery = "Batería"
 batteryCharge = "Carga la batería"
 batteryDischarge = "Descargar la batería"
-batteryTooltip = "{energy} de {total} ({soc}%)"
+batteryTooltip = "{energy} de {total} ({soc})"
 gridImport = "Consumo de red"
 homePower = "Consumo"
 loadpoints = "Punto de carga | Punto de carga | {count} Puntos de carga"
diff --git a/i18n/it.toml b/i18n/it.toml
index a64f2b4b..fedd7248 100644
--- a/i18n/it.toml
+++ b/i18n/it.toml
@@ -77,7 +77,7 @@ vehicles = "Parcheggio"
 battery = "Batteria"
 batteryCharge = "Carica della batteria"
 batteryDischarge = "Scarico della batteria"
-batteryTooltip = "{energy} di {total} ({soc}%)"
+batteryTooltip = "{energy} di {total} ({soc})"
 gridImport = "Uso della rete"
 homePower = "Consumo"
 loadpoints = "Caricabatterie| Caricabatterie | {count} caricabatterie"
diff --git a/i18n/lt.toml b/i18n/lt.toml
index 59c7487d..6ccfe669 100644
--- a/i18n/lt.toml
+++ b/i18n/lt.toml
@@ -77,7 +77,7 @@ vehicles = "Autoparkas"
 battery = "Kaupiklis"
 batteryCharge = "Kaupiklis įkraunamas"
 batteryDischarge = "Kaupiklis iškraunamas"
-batteryTooltip = "{energy} iš {total} ({soc}%)"
+batteryTooltip = "{energy} iš {total} ({soc})"
 gridImport = "Iš tinklo"
 homePower = "Namo suvartojimas"
 loadpoints = "Įkroviklis | Įkroviklis | {count} įkrovikliai"
diff --git a/i18n/nl.toml b/i18n/nl.toml
index 839d8108..83b6a12c 100644
--- a/i18n/nl.toml
+++ b/i18n/nl.toml
@@ -77,7 +77,7 @@ vehicles = "Parking"
 battery = "Batterij"
 batteryCharge = "Batterij opladen"
 batteryDischarge = "Batterij ontladen"
-batteryTooltip = "{energy} van {total} ({soc}%)"
+batteryTooltip = "{energy} van {total} ({soc})"
 gridImport = "Grid gebruik"
 homePower = "Consumptie"
 loadpoints = "Lader| Lader | {count} laders"
diff --git a/i18n/pl.toml b/i18n/pl.toml
index cf3372c3..da344785 100644
--- a/i18n/pl.toml
+++ b/i18n/pl.toml
@@ -77,7 +77,7 @@ vehicles = "Garaż"
 battery = "Bateria"
 batteryCharge = "Ładowanie baterii"
 batteryDischarge = "Rozładowanie baterii"
-batteryTooltip = "{energy} z {total} ({soc}%)"
+batteryTooltip = "{energy} z {total} ({soc})"
 gridImport = "Z sieci"
 homePower = "Zużycie"
 loadpoints = "Ładowarka | Ładowarka | {count} Ładowarki"
diff --git a/i18n/pt.toml b/i18n/pt.toml
index 2662aedf..4d9e898a 100644
--- a/i18n/pt.toml
+++ b/i18n/pt.toml
@@ -77,7 +77,7 @@ vehicles = "Estacionamento"
 battery = "Bateria"
 batteryCharge = "Carrego de bateria"
 batteryDischarge = "Descarrego de bateria"
-batteryTooltip = "{energy} de {total} ({soc}%)"
+batteryTooltip = "{energy} de {total} ({soc})"
 gridImport = "Consumo de rede"
 homePower = "Consumo"
 loadpoints = "Carregador | Carregador | {count} Carregadores"
diff --git a/i18n/ro.toml b/i18n/ro.toml
index 060d6e71..ae0c7f0e 100644
--- a/i18n/ro.toml
+++ b/i18n/ro.toml
@@ -72,7 +72,7 @@ vehicles = "Parcare"
 battery = "Baterie"
 batteryCharge = "Încărcare baterie"
 batteryDischarge = "Descărcare baterie"
-batteryTooltip = "{energy} din {total} ({soc}%)"
+batteryTooltip = "{energy} din {total} ({soc})"
 gridImport = "Folosirea rețelei"
 homePower = "Consum"
 loadpoints = "Încărcător| Încărcător | {count} încărcătoare"
diff --git a/i18n/uk.toml b/i18n/uk.toml
index 04068714..e983ab32 100644
--- a/i18n/uk.toml
+++ b/i18n/uk.toml
@@ -73,7 +73,7 @@ vehicles = "Паркування"
 battery = "Батарея"
 batteryCharge = "Зарядження батареї"
 batteryDischarge = "Розрядження батареї"
-batteryTooltip = "{energy} із {total} ({soc}%)"
+batteryTooltip = "{energy} із {total} ({soc})"
 gridImport = "Використання мережі"
 homePower = "Споживання"
 loadpoints = "Зарядний пристрій| Зарядний пристрій | {count} зарядні пристрої"
diff --git a/meter/fritzdect.go b/meter/fritzdect.go
index d84855f0..925a1346 100644
--- a/meter/fritzdect.go
+++ b/meter/fritzdect.go
@@ -20,5 +20,9 @@ func NewFritzDECTFromConfig(other map[string]interface{}) (api.Meter, error) {
 		return nil, err
 	}
 
+	if cc.User == "" || cc.Password == "" {
+		return nil, api.ErrMissingCredentials
+	}
+
 	return fritzdect.NewConnection(cc.URI, cc.AIN, cc.User, cc.Password)
 }
diff --git a/meter/homematic.go b/meter/homematic.go
index 0b417f9f..dcbece84 100644
--- a/meter/homematic.go
+++ b/meter/homematic.go
@@ -32,6 +32,10 @@ func NewCCUFromConfig(other map[string]interface{}) (api.Meter, error) {
 		return nil, err
 	}
 
+	if cc.User == "" || cc.Password == "" {
+		return nil, api.ErrMissingCredentials
+	}
+
 	return NewCCU(cc.URI, cc.Device, cc.MeterChannel, cc.SwitchChannel, cc.User, cc.Password, cc.Usage)
 }
 
diff --git a/meter/powerwall.go b/meter/powerwall.go
index 57b3b522..602c841a 100644
--- a/meter/powerwall.go
+++ b/meter/powerwall.go
@@ -8,6 +8,7 @@ import (
 	"time"
 
 	"github.com/evcc-io/evcc/api"
+	"github.com/evcc-io/evcc/provider"
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/request"
 	"github.com/foogod/go-powerwall"
@@ -17,6 +18,7 @@ import (
 type PowerWall struct {
 	usage  string
 	client *powerwall.Client
+	meterG func() (map[string]powerwall.MeterAggregatesData, error)
 }
 
 func init() {
@@ -28,8 +30,11 @@ func init() {
 
 // NewPowerWallFromConfig creates a PowerWall Powerwall Meter from generic config
 func NewPowerWallFromConfig(other map[string]interface{}) (api.Meter, error) {
-	var cc struct {
+	cc := struct {
 		URI, Usage, User, Password string
+		Cache                      time.Duration
+	}{
+		Cache: time.Second,
 	}
 
 	if err := util.DecodeOther(other, &cc); err != nil {
@@ -52,11 +57,11 @@ func NewPowerWallFromConfig(other map[string]interface{}) (api.Meter, error) {
 		cc.Usage = "solar"
 	}
 
-	return NewPowerWall(cc.URI, cc.Usage, cc.User, cc.Password)
+	return NewPowerWall(cc.URI, cc.Usage, cc.User, cc.Password, cc.Cache)
 }
 
 // NewPowerWall creates a Tesla PowerWall Meter
-func NewPowerWall(uri, usage, user, password string) (api.Meter, error) {
+func NewPowerWall(uri, usage, user, password string, cache time.Duration) (api.Meter, error) {
 	log := util.NewLogger("powerwall").Redact(user, password)
 
 	httpClient := &http.Client{
@@ -72,6 +77,7 @@ func NewPowerWall(uri, usage, user, password string) (api.Meter, error) {
 	m := &PowerWall{
 		client: client,
 		usage:  strings.ToLower(usage),
+		meterG: provider.Cached(client.GetMetersAggregates, cache),
 	}
 
 	// decorate api.MeterEnergy
@@ -103,7 +109,7 @@ var _ api.Meter = (*PowerWall)(nil)
 
 // CurrentPower implements the api.Meter interface
 func (m *PowerWall) CurrentPower() (float64, error) {
-	res, err := m.client.GetMetersAggregates()
+	res, err := m.meterG()
 	if err != nil {
 		return 0, err
 	}
@@ -117,7 +123,7 @@ func (m *PowerWall) CurrentPower() (float64, error) {
 
 // totalEnergy implements the api.MeterEnergy interface
 func (m *PowerWall) totalEnergy() (float64, error) {
-	res, err := m.client.GetMetersAggregates()
+	res, err := m.meterG()
 	if err != nil {
 		return 0, err
 	}
diff --git a/meter/tapo.go b/meter/tapo.go
index d1dfbd84..83c75d71 100644
--- a/meter/tapo.go
+++ b/meter/tapo.go
@@ -23,5 +23,9 @@ func NewTapoFromConfig(other map[string]interface{}) (api.Meter, error) {
 		return nil, err
 	}
 
+	if cc.User == "" || cc.Password == "" {
+		return nil, api.ErrMissingCredentials
+	}
+
 	return tapo.NewConnection(cc.URI, cc.User, cc.Password)
 }
diff --git a/templates/definition/charger/fritzdect.yaml b/templates/definition/charger/fritzdect.yaml
index ec42a82f..f7e31522 100644
--- a/templates/definition/charger/fritzdect.yaml
+++ b/templates/definition/charger/fritzdect.yaml
@@ -14,8 +14,6 @@ params:
     mask: true
   - name: ain
     required: true
-  - name: standbypower
-    default: 15
   - preset: switchsocket
 render: |
   type: fritzdect
@@ -23,4 +21,4 @@ render: |
   user: {{ .user }}
   password: {{ .password }}
   ain: {{ .ain }} # switch actor identification number without blanks (see AIN number on switch sticker)
-  standbypower: {{ .standbypower }}
+  {{ include "switchsocket" . }}
diff --git a/templates/definition/charger/homematic.yaml b/templates/definition/charger/homematic.yaml
index 3b79b28f..353fbd34 100644
--- a/templates/definition/charger/homematic.yaml
+++ b/templates/definition/charger/homematic.yaml
@@ -19,8 +19,6 @@ params:
     help:
       en: Homematic device id like shown in the CCU web user interface.
       de: Homematic Geräte Id, wie im CCU Webfrontend angezeigt.
-  - name: standbypower
-    default: 15
   - name: user
     required: false
   - name: password
@@ -53,12 +51,12 @@ render: |
   type: homematic
   uri: {{ .host }}:{{ .port }}
   device: {{ .device }}
-  meterchannel: {{ .meterchannel }}
-  switchchannel: {{ .switchchannel }}
-  standbypower: {{ .standbypower }}
   {{ if ne .user "" }}
   user: {{ .user }}
   {{ end }}
   {{ if ne .password "" }}
   password: {{ .password }}
   {{ end }}
+  meterchannel: {{ .meterchannel }}
+  switchchannel: {{ .switchchannel }}
+  {{ include "switchsocket" . }}
diff --git a/templates/definition/charger/mystrom.yaml b/templates/definition/charger/mystrom.yaml
index 09f417a7..c249c15e 100644
--- a/templates/definition/charger/mystrom.yaml
+++ b/templates/definition/charger/mystrom.yaml
@@ -8,9 +8,8 @@ params:
   - name: usage
     choice: ["pv", "charge"]
   - name: host
-  - name: standbypower
-    default: 15
+  - preset: switchsocket
 render: |
   type: mystrom
   uri: http://{{ .host }}
-  standbypower: {{ .standbypower }}
+  {{ include "switchsocket" . }}
diff --git a/templates/definition/charger/ocpp.yaml b/templates/definition/charger/ocpp.yaml
index 3a1db706..12c64d2e 100644
--- a/templates/definition/charger/ocpp.yaml
+++ b/templates/definition/charger/ocpp.yaml
@@ -49,12 +49,6 @@ params:
     help:
       en: Connector number, usually 1 for first connector.
       de: Verwendeter Ladepunkt, normalerweise 1 für den ersten Anschluss.
-  - name: meter
-    default: false
-    type: bool
-    help:
-      en: Use integrated meter (if present)
-      de: Benutze den integrierten Zähler (falls vorhanden)
   - name: idtag
     type: string
     advanced: true
@@ -75,7 +69,4 @@ render: |
   {{- if ne .idtag "" }}
   idtag: {{ .idtag }}
   {{- end }}
-  {{- if ne .meter "false" }}
-  meter: {{ .meter }}
-  {{- end }}
   timeout: {{ .timeout }}
diff --git a/templates/definition/charger/shelly.yaml b/templates/definition/charger/shelly.yaml
index 3074a8cf..700ba02c 100644
--- a/templates/definition/charger/shelly.yaml
+++ b/templates/definition/charger/shelly.yaml
@@ -9,8 +9,6 @@ params:
     mask: true
   - name: channel
     default: 0
-  - name: standbypower
-    default: 15
   - preset: switchsocket
 render: |
   type: shelly
@@ -18,4 +16,4 @@ render: |
   {{ if ne .user "" }}user: {{ .user }}{{ end }}
   {{ if ne .password "" }}password: {{ .password }}{{ end }}
   channel: {{ .channel }}  # shelly device relay channel
-  standbypower: {{ .standbypower }}
+  {{ include "switchsocket" . }}
diff --git a/templates/definition/charger/tapo.yaml b/templates/definition/charger/tapo.yaml
index 91e74620..8fb60ecd 100644
--- a/templates/definition/charger/tapo.yaml
+++ b/templates/definition/charger/tapo.yaml
@@ -11,12 +11,10 @@ params:
   - name: password
     required: true
     mask: true
-  - name: standbypower
-    default: 15
   - preset: switchsocket
 render: |
   type: tapo
   uri: http://{{ .host }}
   user: {{ .user }}
   password: {{ .password }}
-  standbypower: {{ .standbypower }}
+  {{ include "switchsocket" . }}
diff --git a/templates/definition/charger/tasmota.yaml b/templates/definition/charger/tasmota.yaml
index 61a70f81..39f621fa 100644
--- a/templates/definition/charger/tasmota.yaml
+++ b/templates/definition/charger/tasmota.yaml
@@ -22,8 +22,6 @@ params:
     help:
       de: Nummer des Schaltkanals (1-8), bei Geräten mit mehr als einem Schalter
       en: Device relay channel number (allowed range 1-8)
-  - name: standbypower
-    default: 15
   - preset: switchsocket
 render: |
   type: tasmota
@@ -35,4 +33,4 @@ render: |
   password: {{ .password }}
   {{ end }}
   channel: {{ .channel }}  # relay channel (allowed range: 1-8)
-  standbypower: {{ .standbypower }}
+  {{ include "switchsocket" . }}
diff --git a/templates/definition/charger/tplink.yaml b/templates/definition/charger/tplink.yaml
index 7beac61f..ca017317 100644
--- a/templates/definition/charger/tplink.yaml
+++ b/templates/definition/charger/tplink.yaml
@@ -6,10 +6,8 @@ products:
 group: switchsockets
 params:
   - name: host
-  - name: standbypower
-    default: 15
   - preset: switchsocket
 render: |
   type: tplink
   uri: {{ .host }}
-  standbypower: {{ .standbypower }}
+  {{ include "switchsocket" . }}
diff --git a/templates/definition/meter/janitza-b23.yaml b/templates/definition/meter/janitza.yaml
similarity index 87%
rename from templates/definition/meter/janitza-b23.yaml
rename to templates/definition/meter/janitza.yaml
index fd52f669..b163d9cf 100644
--- a/templates/definition/meter/janitza-b23.yaml
+++ b/templates/definition/meter/janitza.yaml
@@ -1,8 +1,8 @@
-template: janitza-b23
+template: janitza
 products:
   - brand: Janitza
     description:
-      generic: B23
+      generic: B series, UMG series
 params:
   - name: usage
     choice: ["grid", "charge"]
@@ -11,6 +11,7 @@ params:
 render: |
   type: modbus
   {{- include "modbus" . }}
+  model: janitza
   power: Power
   energy: Import
   currents:
diff --git a/templates/docs/charger/homematic_0.yaml b/templates/docs/charger/homematic_0.yaml
index 7d8b1530..62a4c2b3 100644
--- a/templates/docs/charger/homematic_0.yaml
+++ b/templates/docs/charger/homematic_0.yaml
@@ -8,19 +8,19 @@ render:
       host: 192.0.2.2 # IP-Adresse oder Hostname
       port: 2010 # Port # Optional
       device: '0001EE89AAD848' # Homematic Geräte Id, wie im CCU Webfrontend angezeigt.
-      standbypower: 15 # Leistung oberhalb des angegebenen Wertes wird als Ladeleistung gewertet # Optional
       user: # Benutzerkonto (bspw. E-Mail Adresse, User Id, etc.) # Optional
       password: # Passwort des Benutzerkontos (bei führenden Nullen bitte in einfache Hochkommata setzen) # Optional
+      standbypower: 15 # Leistung oberhalb des angegebenen Wertes wird als Ladeleistung gewertet # Optional
     advanced: |
       type: template
       template: homematic
       host: 192.0.2.2 # IP-Adresse oder Hostname
       port: 2010 # Port # Optional
       device: '0001EE89AAD848' # Homematic Geräte Id, wie im CCU Webfrontend angezeigt.
-      standbypower: 15 # Leistung oberhalb des angegebenen Wertes wird als Ladeleistung gewertet # Optional
       user: # Benutzerkonto (bspw. E-Mail Adresse, User Id, etc.) # Optional
       password: # Passwort des Benutzerkontos (bei führenden Nullen bitte in einfache Hochkommata setzen) # Optional
       meterchannel: 6 # Kanalnummer des Messwertkanals, wie im CCU Webfrontend angezeigt.
       switchchannel: 3 # Kanalnummer der schaltbaren Steckdose, wie im CCU Webfrontend angezeigt.
+      standbypower: 15 # Leistung oberhalb des angegebenen Wertes wird als Ladeleistung gewertet # Optional
       integrateddevice: # Optional
       icon: # Icon in der Benutzeroberfläche # Optional
diff --git a/templates/docs/charger/mystrom_0.yaml b/templates/docs/charger/mystrom_0.yaml
index 4e48d455..df261550 100644
--- a/templates/docs/charger/mystrom_0.yaml
+++ b/templates/docs/charger/mystrom_0.yaml
@@ -10,6 +10,14 @@ render:
       usage: pv
       host: 192.0.2.2 # IP-Adresse oder Hostname
       standbypower: 15 # Leistung oberhalb des angegebenen Wertes wird als Ladeleistung gewertet # Optional
+    advanced: |
+      type: template
+      template: mystrom
+      usage: pv
+      host: 192.0.2.2 # IP-Adresse oder Hostname
+      standbypower: 15 # Leistung oberhalb des angegebenen Wertes wird als Ladeleistung gewertet # Optional
+      integrateddevice: # Optional
+      icon: # Icon in der Benutzeroberfläche # Optional
   - usage: charge
     default: |
       type: template
@@ -17,3 +25,11 @@ render:
       usage: charge
       host: 192.0.2.2 # IP-Adresse oder Hostname
       standbypower: 15 # Leistung oberhalb des angegebenen Wertes wird als Ladeleistung gewertet # Optional
+    advanced: |
+      type: template
+      template: mystrom
+      usage: charge
+      host: 192.0.2.2 # IP-Adresse oder Hostname
+      standbypower: 15 # Leistung oberhalb des angegebenen Wertes wird als Ladeleistung gewertet # Optional
+      integrateddevice: # Optional
+      icon: # Icon in der Benutzeroberfläche # Optional
diff --git a/templates/docs/charger/ocpp_0.yaml b/templates/docs/charger/ocpp_0.yaml
index d9af7fca..3c7ba848 100644
--- a/templates/docs/charger/ocpp_0.yaml
+++ b/templates/docs/charger/ocpp_0.yaml
@@ -20,12 +20,10 @@ render:
   - default: |
       type: template
       template: ocpp
-      meter: false # Benutze den integrierten Zähler (falls vorhanden) # Optional
     advanced: |
       type: template
       template: ocpp
       stationid: EVB-P12354 # Die Stations-ID der Wallbox (oder des Ladepunkts) # Optional
       connector: 1 # Verwendeter Ladepunkt, normalerweise 1 für den ersten Anschluss. # Optional
-      meter: false # Benutze den integrierten Zähler (falls vorhanden) # Optional
       idtag: '04E6B78921BBA0' # Token-ID welche für die Freischaltung der Ladevorgänge an den Ladepunkt zurückgesendet wird # Optional
       timeout: 10m # Optional
diff --git a/templates/docs/meter/janitza-b23_0.yaml b/templates/docs/meter/janitza_0.yaml
similarity index 93%
rename from templates/docs/meter/janitza-b23_0.yaml
rename to templates/docs/meter/janitza_0.yaml
index 4f1d11c1..aea8f910 100644
--- a/templates/docs/meter/janitza-b23_0.yaml
+++ b/templates/docs/meter/janitza_0.yaml
@@ -1,11 +1,11 @@
 product:
   brand: Janitza
-  description: B23
+  description: B series, UMG series
 render:
   - usage: grid
     default: |
       type: template
-      template: janitza-b23
+      template: janitza
       usage: grid      
 
       # RS485 via adapter (Modbus RTU)
@@ -29,7 +29,7 @@ render:
   - usage: charge
     default: |
       type: template
-      template: janitza-b23
+      template: janitza
       usage: charge      
 
       # RS485 via adapter (Modbus RTU)
diff --git a/util/templates/defaults.yaml b/util/templates/defaults.yaml
index c23b3c71..2bbb5a3a 100644
--- a/util/templates/defaults.yaml
+++ b/util/templates/defaults.yaml
@@ -338,6 +338,8 @@ presets:
       - name: ip
   switchsocket:
     params:
+      - name: standbypower
+        default: 15
       - name: integrateddevice
         type: bool
         advanced: true
diff --git a/util/templates/includes/switchsocket.tpl b/util/templates/includes/switchsocket.tpl
index b30f61df..0f54fe3e 100644
--- a/util/templates/includes/switchsocket.tpl
+++ b/util/templates/includes/switchsocket.tpl
@@ -1,4 +1,5 @@
 {{define "switchsocket"}}
+standbypower: {{ .standbypower }}
 {{ if eq .integrateddevice "true" }}features: ["integrateddevice"]{{ end }}
 {{ if ne .icon "" }}icon: {{ .icon }}{{ end }}
 {{end}}
diff --git a/util/templates/includes/vehicle-identify.tpl b/util/templates/includes/vehicle-identify.tpl
index ca54083e..b425bdf9 100644
--- a/util/templates/includes/vehicle-identify.tpl
+++ b/util/templates/includes/vehicle-identify.tpl
@@ -17,7 +17,7 @@ onIdentify:
   maxCurrent: {{ .maxCurrent }}
 {{- end }}
 {{- if (ne .priority "") }}
-- priority: {{ .priority }}
+  priority: {{ .priority }}
 {{- end }}
 {{- end }}
 {{- if ne (len .identifiers) 0 }}
diff --git a/util/templates/template.go b/util/templates/template.go
index 2a923068..1982e7b0 100644
--- a/util/templates/template.go
+++ b/util/templates/template.go
@@ -323,7 +323,12 @@ func (t *Template) RenderResult(renderMode string, other map[string]interface{})
 
 		default:
 			if res[out] == nil || res[out].(string) == "" {
-				res[out] = yamlQuote(fmt.Sprintf("%v", val))
+				// prevent rendering nil interfaces as "<nil>" string
+				var s string
+				if val != nil {
+					s = yamlQuote(fmt.Sprintf("%v", val))
+				}
+				res[out] = s
 			}
 		}
 	}


Test patch:
diff --git a/api/actionconfig_test.go b/api/actionconfig_test.go
new file mode 100644
index 00000000..1286b8ef
--- /dev/null
+++ b/api/actionconfig_test.go
@@ -0,0 +1,44 @@
+package api
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestMerge(t *testing.T) {
+	pv := ModePV
+	one := 1
+	six := 6.0
+	old := ActionConfig{
+		Mode:       &pv,
+		Priority:   &one,
+		MinCurrent: &six,
+	}
+
+	now := ModeNow
+	two := 2
+	three := 3
+	new := ActionConfig{
+		Mode:     &now,
+		MinSoc:   &three,
+		Priority: &two,
+	}
+
+	dst := old.Merge(new)
+
+	// unmodified
+	assert.Equal(t, old, ActionConfig{
+		Mode:       &pv,
+		Priority:   &one,
+		MinCurrent: &six,
+	}, "old modified")
+
+	// overwritten
+	assert.Equal(t, dst, ActionConfig{
+		Mode:       &now,
+		MinCurrent: &six,
+		MinSoc:     &three,
+		Priority:   &two,
+	}, "new wrong")
+}
diff --git a/util/decoder_test.go b/util/decoder_test.go
new file mode 100644
index 00000000..b78eb42c
--- /dev/null
+++ b/util/decoder_test.go
@@ -0,0 +1,36 @@
+package util
+
+import (
+	"testing"
+
+	"github.com/mitchellh/mapstructure"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestDecodeNil(t *testing.T) {
+	var dst struct {
+		User, Password string
+	}
+
+	decoderConfig := &mapstructure.DecoderConfig{
+		Result:           &dst,
+		ErrorUnused:      true,
+		WeaklyTypedInput: true,
+		DecodeHook: mapstructure.ComposeDecodeHookFunc(
+			mapstructure.StringToTimeDurationHookFunc(),
+			mapstructure.TextUnmarshallerHookFunc(),
+		),
+	}
+
+	decoder, err := mapstructure.NewDecoder(decoderConfig)
+	assert.NoError(t, err)
+
+	err = decoder.Decode(map[string]any{
+		"user": nil,
+	})
+	assert.NoError(t, err)
+
+	assert.Equal(t, struct {
+		User, Password string
+	}{}, dst)
+}