Repository: evcc-io/evcc
Pull request #4883 (2022-10-20 12:56:24)

Add sunspec command for dumping model contents


Patch:
diff --git a/api/store/types.go b/api/store/types.go
new file mode 100644
index 00000000..33b652a6
--- /dev/null
+++ b/api/store/types.go
@@ -0,0 +1,10 @@
+package store
+
+// Provider creates a Persister for given string key
+type Provider func(string) Store
+
+// Store can load and store data
+type Store interface {
+	Load(any) error
+	Save(any) error
+}
diff --git a/cmd/configure.go b/cmd/configure.go
index 640b2430..bd3b72de 100644
--- a/cmd/configure.go
+++ b/cmd/configure.go
@@ -15,7 +15,7 @@ import (
 // configureCmd represents the configure command
 var configureCmd = &cobra.Command{
 	Use:   "configure",
-	Short: "Create an EVCC configuration",
+	Short: "Create configuration (evcc.yaml)",
 	Run:   runConfigure,
 }
 
diff --git a/cmd/flags.go b/cmd/flags.go
index e4dbb473..19c3976b 100644
--- a/cmd/flags.go
+++ b/cmd/flags.go
@@ -9,7 +9,7 @@ import (
 
 const (
 	flagSqlite            = "sqlite"
-	flagSqliteDescription = "Sqlite database file"
+	flagSqliteDescription = "SQlite database file"
 
 	flagHeaders            = "log-headers"
 	flagHeadersDescription = "Log headers"
diff --git a/cmd/setup.go b/cmd/setup.go
index 71402298..c4fc052a 100644
--- a/cmd/setup.go
+++ b/cmd/setup.go
@@ -19,6 +19,7 @@ import (
 	"github.com/evcc-io/evcc/push"
 	"github.com/evcc-io/evcc/server"
 	"github.com/evcc-io/evcc/server/db"
+	"github.com/evcc-io/evcc/server/db/settings"
 	"github.com/evcc-io/evcc/tariff"
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/locale"
@@ -74,7 +75,7 @@ func configureEnvironment(cmd *cobra.Command, conf config) (err error) {
 	}
 
 	// setup sponsorship
-	if conf.SponsorToken != "" {
+	if err == nil && conf.SponsorToken != "" {
 		err = sponsor.ConfigureSponsorship(conf.SponsorToken)
 	}
 
@@ -117,7 +118,17 @@ func configureEnvironment(cmd *cobra.Command, conf config) (err error) {
 
 // configureDatabase configures session database
 func configureDatabase(conf dbConfig) error {
-	return db.NewInstance(conf.Type, conf.Dsn)
+	err := db.NewInstance(conf.Type, conf.Dsn)
+	if err == nil {
+		if err = settings.Init(); err == nil {
+			shutdown.Register(func() {
+				if err := settings.Persist(); err != nil {
+					log.ERROR.Println("cannot save settings:", err)
+				}
+			})
+		}
+	}
+	return err
 }
 
 // configureInflux configures influx database
diff --git a/cmd/sunspec.go b/cmd/sunspec.go
new file mode 100644
index 00000000..eace9a49
--- /dev/null
+++ b/cmd/sunspec.go
@@ -0,0 +1,102 @@
+package cmd
+
+import (
+	"fmt"
+	"os"
+	"strings"
+	"text/tabwriter"
+
+	sunspec "github.com/andig/gosunspec"
+	bus "github.com/andig/gosunspec/modbus"
+	"github.com/andig/gosunspec/smdx"
+	"github.com/evcc-io/evcc/util"
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+	"github.com/volkszaehler/mbmd/meters"
+	quirks "github.com/volkszaehler/mbmd/meters/sunspec"
+)
+
+// sunspecCmd represents the charger command
+var sunspecCmd = &cobra.Command{
+	Use:   "sunspec <connection>",
+	Short: "Dump SunSpec model information",
+	Args:  cobra.ExactArgs(1),
+	Run:   runSunspec,
+}
+
+var slaveID *int
+
+func init() {
+	rootCmd.AddCommand(sunspecCmd)
+
+	slaveID = sunspecCmd.Flags().IntP("id", "i", 1, "Slave id")
+}
+
+func pf(format string, v ...interface{}) {
+	format = strings.TrimSuffix(format, "\n") + "\n"
+	fmt.Printf(format, v...)
+}
+
+func modelName(m sunspec.Model) string {
+	model := smdx.GetModel(uint16(m.Id()))
+	if model == nil {
+		return ""
+	}
+	return model.Name
+}
+
+func runSunspec(cmd *cobra.Command, args []string) {
+	util.LogLevel(viper.GetString("log"), nil)
+
+	conn := meters.NewTCP(args[0])
+	conn.Slave(uint8(*slaveID))
+	conn.Logger(log.TRACE)
+
+	in, err := bus.Open(conn.ModbusClient())
+	if err != nil && in == nil {
+		log.FATAL.Fatal(err)
+	} else if err != nil {
+		log.WARN.Printf("warning: device opened with partial result: %v", err) // log error but continue
+	}
+
+	tw := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
+
+	in.Do(func(d sunspec.Device) {
+		d.Do(func(m sunspec.Model) {
+			pf("--------- Model %d %s ---------", m.Id(), modelName(m))
+
+			blocknum := 0
+			m.Do(func(b sunspec.Block) {
+				if blocknum > 0 {
+					fmt.Fprintf(tw, "-- Block %d --\n", blocknum)
+				}
+				blocknum++
+
+				err = b.Read()
+				if err != nil {
+					log.INFO.Printf("skipping due to read error: %v", err)
+					return
+				}
+
+				b.Do(func(p sunspec.Point) {
+					t := p.Type()[0:3]
+					v := p.Value()
+					if p.NotImplemented() {
+						v = "n/a"
+					} else if t == "int" || t == "uin" || t == "acc" {
+						// for time being, always to this
+						quirks.FixKostal(p)
+
+						v = p.ScaledValue()
+						v = fmt.Sprintf("%.2f", v)
+					}
+
+					vs := fmt.Sprintf("%17v", v)
+					fmt.Fprintf(tw, "%s\t%s\t   %s\n", p.Id(), vs, p.Type())
+				})
+			})
+
+			tw.Flush()
+		})
+	})
+}
diff --git a/docker/bin/entrypoint.sh b/docker/bin/entrypoint.sh
index 98ac5e42..280929f3 100755
--- a/docker/bin/entrypoint.sh
+++ b/docker/bin/entrypoint.sh
@@ -5,15 +5,22 @@ set -e
 HASSIO_OPTIONSFILE=/data/options.json
 
 if [ -f ${HASSIO_OPTIONSFILE} ]; then
-    CONFIG=$(grep config_file ${HASSIO_OPTIONSFILE}| cut -d ':' -f 2 | sed s/[\"}]//g )
+    CONFIG=$(grep -o '"config_file": "[^"]*' ${HASSIO_OPTIONSFILE} | grep -o '[^"]*$')
     echo "Using config file: ${CONFIG}"
 
-    if [ ! -f ${CONFIG} ]; then
+    SQLITE_FILE=$(grep -o '"sqlite_file": "[^"]*' ${HASSIO_OPTIONSFILE} | grep -o '[^"]*$')
+
+    if [ ! -f "${CONFIG}" ]; then
         echo "Config not found. Please create a config under ${CONFIG}."
         echo "For details see evcc documentation at https://github.com/evcc-io/evcc#readme."
     else
-        echo "starting evcc: 'evcc --config ${CONFIG}'"
-        exec evcc --config ${CONFIG}
+        if [ "${SQLITE_FILE}" ]; then
+            echo "starting evcc: 'evcc --config ${CONFIG} --sqlite ${SQLITE_FILE}'"
+            exec evcc --config "${CONFIG}" --sqlite "${SQLITE_FILE}"
+        else
+            echo "starting evcc: 'evcc --config ${CONFIG}'"
+            exec evcc --config "${CONFIG}"
+        fi
     fi
 else
     if [ "$1" == '"evcc"' ] || expr "$1" : '-*' > /dev/null; then
diff --git a/go.mod b/go.mod
index 50e18a49..f45bdaaa 100644
--- a/go.mod
+++ b/go.mod
@@ -7,9 +7,10 @@ require (
 	github.com/BurntSushi/toml v1.2.0
 	github.com/Masterminds/sprig/v3 v3.2.2
 	github.com/PuerkitoBio/goquery v1.8.0
+	github.com/andig/gosunspec v0.0.0-20211108155140-af2e73b86e71
 	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.118
+	github.com/aws/aws-sdk-go v1.44.119
 	github.com/basgys/goxml2json v1.1.0
 	github.com/basvdlei/gotsmart v0.0.3
 	github.com/benbjohnson/clock v1.3.0
@@ -25,7 +26,7 @@ require (
 	github.com/dylanmei/iso8601 v0.1.0
 	github.com/eclipse/paho.mqtt.golang v1.4.1
 	github.com/emirpasic/gods v1.18.1
-	github.com/evcc-io/eebus v0.0.0-20220929204231-3e14a14e2b1c
+	github.com/evcc-io/eebus v0.0.0-20221023111026-e3c9e4d1f3c8
 	github.com/fatih/structs v1.1.0
 	github.com/foogod/go-powerwall v0.2.0
 	github.com/glebarez/sqlite v1.5.0
@@ -82,9 +83,9 @@ require (
 	github.com/volkszaehler/mbmd v0.0.0-20220916220750-3b12dcc33299
 	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-20221012211006-4de253d81b95
-	golang.org/x/net v0.0.0-20221017152216-f25eb7ecb193
-	golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783
+	golang.org/x/exp v0.0.0-20221019170559-20944726eadf
+	golang.org/x/net v0.1.0
+	golang.org/x/oauth2 v0.1.0
 	golang.org/x/text v0.4.0
 	google.golang.org/api v0.100.0
 	google.golang.org/grpc v1.50.1
@@ -97,7 +98,6 @@ require (
 	cloud.google.com/go/compute v1.10.0 // indirect
 	github.com/Masterminds/goutils v1.1.1 // indirect
 	github.com/Masterminds/semver/v3 v3.1.1 // indirect
-	github.com/andig/gosunspec v0.0.0-20211108155140-af2e73b86e71 // indirect
 	github.com/andybalholm/cascadia v1.3.1 // indirect
 	github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
@@ -165,11 +165,11 @@ require (
 	github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
 	github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
 	go.opencensus.io v0.23.0 // indirect
-	golang.org/x/crypto v0.0.0-20221012134737-56aed061732a // indirect
-	golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
+	golang.org/x/crypto v0.1.0 // indirect
+	golang.org/x/mod v0.6.0 // indirect
 	golang.org/x/sync v0.1.0 // indirect
 	golang.org/x/sys v0.1.0 // indirect
-	golang.org/x/term v0.0.0-20221017184919-83659145692c // indirect
+	golang.org/x/term v0.1.0 // indirect
 	golang.org/x/tools v0.1.12 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/genproto v0.0.0-20221018160656-63c7b68cfc55 // indirect
diff --git a/go.sum b/go.sum
index 314da0c8..0e621ace 100644
--- a/go.sum
+++ b/go.sum
@@ -113,8 +113,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.118 h1:FJOqIRTukf7+Ulp047/k7JB6eqMXNnj7eb+coORThHQ=
-github.com/aws/aws-sdk-go v1.44.118/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
+github.com/aws/aws-sdk-go v1.44.119 h1:TPkpDsanBMcZaF5wHwpKhjkapRV/b7d2qdC+a+IPbmY=
+github.com/aws/aws-sdk-go v1.44.119/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
 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=
@@ -228,8 +228,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m
 github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
 github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/evcc-io/eebus v0.0.0-20220929204231-3e14a14e2b1c h1:WVQ0xywNGq+v+npAoHLNhN8Mb+ePD0t0JImCbplz3Wc=
-github.com/evcc-io/eebus v0.0.0-20220929204231-3e14a14e2b1c/go.mod h1:kkKB+GC+F1XOFdy68BCwRbLPuFUqAZP3UIk4Vhr/FYA=
+github.com/evcc-io/eebus v0.0.0-20221023111026-e3c9e4d1f3c8 h1:0qWgzAMPCjGmSVucu/DJkPWk5KPnrD7O114dnSBi+uY=
+github.com/evcc-io/eebus v0.0.0-20221023111026-e3c9e4d1f3c8/go.mod h1:kkKB+GC+F1XOFdy68BCwRbLPuFUqAZP3UIk4Vhr/FYA=
 github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
@@ -944,8 +944,8 @@ golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5y
 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.0.0-20221012134737-56aed061732a h1:NmSIgad6KjE6VvHciPZuNRTKxGhlPfD6OA87W/PLkqg=
-golang.org/x/crypto v0.0.0-20221012134737-56aed061732a/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
+golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -956,8 +956,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-20221012211006-4de253d81b95 h1:sBdrWpxhGDdTAYNqbgBLAR+ULAPPhfgncLr1X0lyWtg=
-golang.org/x/exp v0.0.0-20221012211006-4de253d81b95/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
+golang.org/x/exp v0.0.0-20221019170559-20944726eadf h1:nFVjjKDgNY37+ZSYCJmtYf7tOlfQswHqplG2eosjOMg=
+golang.org/x/exp v0.0.0-20221019170559-20944726eadf/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
 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=
@@ -985,8 +985,8 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 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 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
+golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
 golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1054,8 +1054,8 @@ golang.org/x/net v0.0.0-20220513224357-95641704303c/go.mod h1:CfG3xpIq0wQ8r1q4Su
 golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
-golang.org/x/net v0.0.0-20221017152216-f25eb7ecb193 h1:3Moaxt4TfzNcQH6DWvlYKraN1ozhBXQHcgvXjRGeim0=
-golang.org/x/net v0.0.0-20221017152216-f25eb7ecb193/go.mod h1:RpDiru2p0u2F0lLpEoqnP2+7xs0ifAuOcJ442g6GU2s=
+golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
+golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
 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.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1077,8 +1077,8 @@ golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j
 golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
 golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
 golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
-golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 h1:nt+Q6cXKz4MosCSpnbMtqiQ8Oz0pxTef2B4Vca2lvfk=
-golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
+golang.org/x/oauth2 v0.1.0 h1:isLCZuhj4v+tYv7eskaN4v/TM+A1begWWgyVJDdl1+Y=
+golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A=
 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=
@@ -1200,8 +1200,8 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.0.0-20221017184919-83659145692c h1:dveknrit5futqEmXAvd2I1BbZIDhxRijsyWHM86NlcA=
-golang.org/x/term v0.0.0-20221017184919-83659145692c/go.mod h1:VTIZ7TEbF0BS9Sv9lPTvGbtW8i4z6GGbJBCM37uMCzY=
+golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
+golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
diff --git a/server/db/settings/setting.go b/server/db/settings/setting.go
new file mode 100644
index 00000000..f21f8903
--- /dev/null
+++ b/server/db/settings/setting.go
@@ -0,0 +1,105 @@
+package settings
+
+import (
+	"encoding/json"
+	"errors"
+	"strconv"
+	"sync/atomic"
+
+	"github.com/evcc-io/evcc/server/db"
+	"golang.org/x/exp/slices"
+)
+
+var ErrNotFound = errors.New("not found")
+
+// setting is a settings entry
+type setting struct {
+	Key   string `json:"key" gorm:"primarykey"`
+	Value string `json:"value"`
+}
+
+var (
+	settings []setting
+	dirty    int32
+)
+
+func Init() error {
+	err := db.Instance.AutoMigrate(new(setting))
+	if err == nil {
+		err = db.Instance.Find(&settings).Error
+	}
+	return err
+}
+
+func Persist() error {
+	dirty := atomic.CompareAndSwapInt32(&dirty, 1, 0)
+	if !dirty || len(settings) == 0 {
+		// avoid "empty slice found"
+		return nil
+	}
+	return db.Instance.Save(settings).Error
+}
+
+func SetString(key string, val string) {
+	idx := slices.IndexFunc(settings, func(s setting) bool {
+		return s.Key == key
+	})
+
+	if idx < 0 {
+		settings = append(settings, setting{key, val})
+		atomic.StoreInt32(&dirty, 1)
+	} else if settings[idx].Value != val {
+		settings[idx].Value = val
+		atomic.StoreInt32(&dirty, 1)
+	}
+}
+
+func SetInt(key string, val int64) {
+	SetString(key, strconv.FormatInt(val, 10))
+}
+
+func SetFloat(key string, val float64) {
+	SetString(key, strconv.FormatFloat(val, 'f', -1, 64))
+}
+
+func SetJson(key string, val any) error {
+	b, err := json.Marshal(val)
+	if err == nil {
+		SetString(key, string(b))
+	}
+	return err
+}
+
+func String(key string) (string, error) {
+	idx := slices.IndexFunc(settings, func(s setting) bool {
+		return s.Key == key
+	})
+	if idx < 0 {
+		return "", ErrNotFound
+	}
+	return settings[idx].Value, nil
+}
+
+func Int(key string) (int64, error) {
+	s, err := String(key)
+	if err != nil {
+		return 0, err
+	}
+	return strconv.ParseInt(s, 10, 64)
+}
+
+func Float(key string) (float64, error) {
+	s, err := String(key)
+	if err != nil {
+		return 0, err
+	}
+	return strconv.ParseFloat(s, 64)
+}
+
+func Json(key string, res any) error {
+	s, err := String(key)
+	if err == nil {
+		err = json.Unmarshal([]byte(s), &res)
+	}
+	return err
+}
diff --git a/server/db/settings/store.go b/server/db/settings/store.go
new file mode 100644
index 00000000..4c0dea76
--- /dev/null
+++ b/server/db/settings/store.go
@@ -0,0 +1,21 @@
+package settings
+
+import "github.com/evcc-io/evcc/api/store"
+
+type storer struct {
+	key string
+}
+
+var _ store.Store = (*storer)(nil)
+
+func NewStore(key string) store.Store {
+	return &storer{key: key}
+}
+
+func (s *storer) Load(res any) error {
+	return Json(s.key, &res)
+}
+
+func (s *storer) Save(val any) error {
+	return SetJson(s.key, val)
+}
diff --git a/templates/definition/meter/huawei-sun2000-dongle-powersensor.yaml b/templates/definition/meter/huawei-sun2000-dongle-powersensor.yaml
index cfc6fd98..342e1ed5 100644
--- a/templates/definition/meter/huawei-sun2000-dongle-powersensor.yaml
+++ b/templates/definition/meter/huawei-sun2000-dongle-powersensor.yaml
@@ -84,7 +84,7 @@ render: |
     register:
       address: 37001
       type: holding
-      decode: int32
+      decode: int32nan
     scale: -1
   soc:
     source: modbus
diff --git a/util/modbus/functions.go b/util/modbus/functions.go
index e3c212b6..bcb98e89 100644
--- a/util/modbus/functions.go
+++ b/util/modbus/functions.go
@@ -53,28 +53,37 @@ func decodeBool16(mask uint64) func(b []byte) float64 {
 	}
 }
 
-func decodeNaN16(nan uint16, f func(b []byte) float64) func(b []byte) float64 {
+func decodeNaN16(f func(b []byte) float64, nan ...uint16) func(b []byte) float64 {
 	return func(b []byte) float64 {
-		if binary.BigEndian.Uint16(b) == nan {
-			return 0
+		u := binary.BigEndian.Uint16(b)
+		for _, nan := range nan {
+			if u == nan {
+				return 0
+			}
 		}
 		return f(b)
 	}
 }
 
-func decodeNaN32(nan uint32, f func(b []byte) float64) func(b []byte) float64 {
+func decodeNaN32(f func(b []byte) float64, nan ...uint32) func(b []byte) float64 {
 	return func(b []byte) float64 {
-		if binary.BigEndian.Uint32(b) == nan {
-			return 0
+		u := binary.BigEndian.Uint32(b)
+		for _, nan := range nan {
+			if u == nan {
+				return 0
+			}
 		}
 		return f(b)
 	}
 }
 
-func decodeNaN64(nan uint64, f func(b []byte) float64) func(b []byte) float64 {
+func decodeNaN64(f func(b []byte) float64, nan ...uint64) func(b []byte) float64 {
 	return func(b []byte) float64 {
-		if binary.BigEndian.Uint64(b) == nan {
-			return 0
+		u := binary.BigEndian.Uint64(b)
+		for _, nan := range nan {
+			if u == nan {
+				return 0
+			}
 		}
 		return f(b)
 	}
diff --git a/util/modbus/modbus.go b/util/modbus/modbus.go
index b872c4f4..c9e19b1c 100644
--- a/util/modbus/modbus.go
+++ b/util/modbus/modbus.go
@@ -309,13 +309,13 @@ func RegisterOperation(r Register) (rs485.Operation, error) {
 		op.Transform = asFloat64(encoding.Int16)
 		op.ReadLen = 1
 	case "int16nan":
-		op.Transform = decodeNaN16(1<<15, asFloat64(encoding.Int16))
+		op.Transform = decodeNaN16(asFloat64(encoding.Int16), 1<<15, 1<<15-1)
 		op.ReadLen = 1
 	case "uint16":
 		op.Transform = asFloat64(encoding.Uint16)
 		op.ReadLen = 1
 	case "uint16nan":
-		op.Transform = decodeNaN16(0xFFFF, asFloat64(encoding.Uint16))
+		op.Transform = decodeNaN16(asFloat64(encoding.Uint16), 1<<16-1)
 		op.ReadLen = 1
 	case "bool16":
 		mask, err := decodeMask(r.BitMask)
@@ -329,7 +329,7 @@ func RegisterOperation(r Register) (rs485.Operation, error) {
 	case "int32":
 		op.Transform = asFloat64(encoding.Int32)
 	case "int32nan":
-		op.Transform = decodeNaN32(1<<31, asFloat64(encoding.Int32))
+		op.Transform = decodeNaN32(asFloat64(encoding.Int32), 1<<31, 1<<31-1)
 	case "int32s":
 		op.Transform = asFloat64(encoding.Int32LswFirst)
 	case "uint32":
@@ -337,7 +337,7 @@ func RegisterOperation(r Register) (rs485.Operation, error) {
 	case "uint32s":
 		op.Transform = asFloat64(encoding.Uint32LswFirst)
 	case "uint32nan":
-		op.Transform = decodeNaN32(0xFFFFFFFF, asFloat64(encoding.Uint32))
+		op.Transform = decodeNaN32(asFloat64(encoding.Uint32), 1<<32-1)
 	case "float32", "ieee754":
 		op.Transform = asFloat64(encoding.Float32)
 	case "float32s", "ieee754s":
@@ -348,7 +348,7 @@ func RegisterOperation(r Register) (rs485.Operation, error) {
 		op.Transform = asFloat64(encoding.Uint64)
 		op.ReadLen = 4
 	case "uint64nan":
-		op.Transform = decodeNaN64(0xFFFFFFFFFFFFFFFF, asFloat64(encoding.Uint64))
+		op.Transform = decodeNaN64(asFloat64(encoding.Uint64), 1<<64-1)
 		op.ReadLen = 4
 	case "float64":
 		op.Transform = encoding.Float64
diff --git a/vehicle/nissan/api.go b/vehicle/nissan/api.go
index b9b279fa..92ddab85 100644
--- a/vehicle/nissan/api.go
+++ b/vehicle/nissan/api.go
@@ -14,7 +14,7 @@ import (
 const (
 	CarAdapterBaseURL  = "https://alliance-platform-caradapter-prod.apps.eu2.kamereon.io/car-adapter"
 	UserAdapterBaseURL = "https://alliance-platform-usersadapter-prod.apps.eu2.kamereon.io/user-adapter"
-	UserBaseURL        = "https://nci-bff-web-prod.apps.eu.kamereon.io/bff-web"
+	UserBaseURL        = "https://nci-bff-web-prod.apps.eu2.kamereon.io/bff-web"
 )
 
 type API struct {
@@ -45,7 +45,7 @@ func (v *API) Vehicles() ([]string, error) {
 
 	var res Vehicles
 	if err == nil {
-		uri := fmt.Sprintf("%s/v4/users/%s/cars", UserBaseURL, user.UserID)
+		uri := fmt.Sprintf("%s/v5/users/%s/cars", UserBaseURL, user.UserID)
 		err = v.GetJSON(uri, &res)
 	}
 


Test patch:
diff --git a/server/db/settings/settings_test.go b/server/db/settings/settings_test.go
new file mode 100644
index 00000000..770e5b7c
--- /dev/null
+++ b/server/db/settings/settings_test.go
@@ -0,0 +1,32 @@
+package settings
+
+import (
+	"math"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestString(t *testing.T) {
+	v := "foo"
+	SetString("string", v)
+	res, err := String("string")
+	assert.Nil(t, err)
+	assert.Equal(t, v, res)
+}
+
+func TestInt(t *testing.T) {
+	v := int64(math.MaxInt64)
+	SetInt("int64", v)
+	res, err := Int("int64")
+	assert.Nil(t, err)
+	assert.Equal(t, v, res)
+}
+
+func TestFloat(t *testing.T) {
+	v := 3.141
+	SetFloat("float64", v)
+	res, err := Float("float64")
+	assert.Nil(t, err)
+	assert.Equal(t, v, res)
+}