diff --git a/api/store/types.go b/api/store/types.go new file mode 100644 index 0000000000..33b652a64b --- /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/setup.go b/cmd/setup.go index 71402298e0..c4fc052aa0 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/server/db/settings/setting.go b/server/db/settings/setting.go new file mode 100644 index 0000000000..f21f890304 --- /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/settings_test.go b/server/db/settings/settings_test.go new file mode 100644 index 0000000000..770e5b7c54 --- /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) +} diff --git a/server/db/settings/store.go b/server/db/settings/store.go new file mode 100644 index 0000000000..4c0dea7699 --- /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) +}