Repository: evcc-io/evcc
Pull request #6196 (2023-02-11 11:04:24)

Http: allow path parameters for setters
Fix https://github.com/evcc-io/evcc/issues/6193


Patch:
diff --git a/core/db/db.go b/core/db/db.go
index 8d38ebee..692ae001 100644
--- a/core/db/db.go
+++ b/core/db/db.go
@@ -20,13 +20,24 @@ type Database interface {
 
 // New creates a database storage driver
 func New(name string) (*DB, error) {
-	db := &DB{
+	db := serverdb.Instance
+
+	// TODO deprecate
+	var err error
+	if table := "transactions"; db.Migrator().HasTable(table) {
+		err = db.Migrator().RenameTable(table, new(Session))
+	}
+	if err == nil {
+		err = db.AutoMigrate(new(Session))
+	}
+
+	sessiondb := &DB{
 		log:  util.NewLogger("db"),
-		db:   serverdb.Instance,
+		db:   db,
 		name: name,
 	}
 
-	return db, nil
+	return sessiondb, err
 }
 
 // Session creates a charging session
@@ -45,3 +56,10 @@ func (s *DB) Persist(session interface{}) {
 		s.log.ERROR.Printf("persist: %v", err)
 	}
 }
+
+// Return sessions
+func (s *DB) Sessions() (Sessions, error) {
+	var res Sessions
+	tx := s.db.Find(&res)
+	return res, tx.Error
+}
diff --git a/core/loadpoint_session.go b/core/loadpoint_session.go
index 91f52257..b5a054d8 100644
--- a/core/loadpoint_session.go
+++ b/core/loadpoint_session.go
@@ -1,8 +1,6 @@
 package core
 
 import (
-	"time"
-
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/core/db"
 )
@@ -55,10 +53,15 @@ func (lp *Loadpoint) stopSession() {
 		return
 	}
 
-	lp.session.Finished = time.Now()
+	lp.session.Finished = lp.clock.Now()
 	lp.session.MeterStop = lp.chargeMeterTotal()
 
-	if chargedEnergy := lp.getChargedEnergy() / 1e3; chargedEnergy > lp.session.ChargedEnergy {
+	chargedEnergy := lp.getChargedEnergy() / 1e3
+	if delta := lp.session.MeterStop - lp.session.MeterStart; delta < chargedEnergy && lp.session.MeterStart*lp.session.MeterStop > 0 {
+		chargedEnergy = delta
+	}
+
+	if chargedEnergy > lp.session.ChargedEnergy {
 		lp.session.ChargedEnergy = chargedEnergy
 	}
 
diff --git a/core/site.go b/core/site.go
index b8f6262b..95ba444a 100644
--- a/core/site.go
+++ b/core/site.go
@@ -110,21 +110,6 @@ func NewSiteFromConfig(
 	site.coordinator = coordinator.New(log, vehicles)
 	site.savings = NewSavings(tariffs)
 
-	// migrate session log
-	if serverdb.Instance != nil {
-		var err error
-		// TODO deprecate
-		if table := "transactions"; serverdb.Instance.Migrator().HasTable(table) {
-			err = serverdb.Instance.Migrator().RenameTable(table, new(db.Session))
-		}
-		if err == nil {
-			err = serverdb.Instance.AutoMigrate(new(db.Session))
-		}
-		if err != nil {
-			return nil, err
-		}
-	}
-
 	// upload telemetry on shutdown
 	if telemetry.Enabled() {
 		shutdown.Register(func() {
diff --git a/provider/http.go b/provider/http.go
index decba6a8..31dd43bf 100644
--- a/provider/http.go
+++ b/provider/http.go
@@ -148,7 +148,7 @@ func (p *HTTP) WithAuth(typ, user, password string) (*HTTP, error) {
 }
 
 // request executes the configured request or returns the cached value
-func (p *HTTP) request(body ...string) ([]byte, error) {
+func (p *HTTP) request(url string, body ...string) ([]byte, error) {
 	if time.Since(p.updated) >= p.cache {
 		var b io.Reader
 		if len(body) == 1 {
@@ -156,7 +156,7 @@ func (p *HTTP) request(body ...string) ([]byte, error) {
 		}
 
 		// empty method becomes GET
-		req, err := request.New(strings.ToUpper(p.method), p.url, b, p.headers)
+		req, err := request.New(strings.ToUpper(p.method), url, b, p.headers)
 		if err != nil {
 			return []byte{}, err
 		}
@@ -200,7 +200,7 @@ func (p *HTTP) IntGetter() func() (int64, error) {
 // StringGetter sends string request
 func (p *HTTP) StringGetter() func() (string, error) {
 	return func() (string, error) {
-		b, err := p.request(p.body)
+		b, err := p.request(p.url, p.body)
 
 		if err == nil && p.pipeline != nil {
 			b, err = p.pipeline.Process(b)
@@ -221,12 +221,18 @@ func (p *HTTP) BoolGetter() func() (bool, error) {
 }
 
 func (p *HTTP) set(param string, val interface{}) error {
-	body, err := setFormattedValue(p.body, param, val)
+	url, err := setFormattedValue(p.url, param, val)
+	if err != nil {
+		return err
+	}
 
-	if err == nil {
-		_, err = p.request(body)
+	body, err := setFormattedValue(p.body, param, val)
+	if err != nil {
+		return err
 	}
 
+	_, err = p.request(url, body)
+
 	return err
 }
 


Test patch:
diff --git a/core/loadpoint_session_test.go b/core/loadpoint_session_test.go
new file mode 100644
index 00000000..95d62494
--- /dev/null
+++ b/core/loadpoint_session_test.go
@@ -0,0 +1,87 @@
+package core
+
+import (
+	"testing"
+	"time"
+
+	"github.com/benbjohnson/clock"
+	"github.com/evcc-io/evcc/api"
+	coredb "github.com/evcc-io/evcc/core/db"
+	"github.com/evcc-io/evcc/mock"
+	serverdb "github.com/evcc-io/evcc/server/db"
+	"github.com/golang/mock/gomock"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestSession(t *testing.T) {
+	var err error
+	serverdb.Instance, err = serverdb.New("sqlite", ":memory:")
+	assert.NoError(t, err)
+
+	db, err := coredb.New("foo")
+	assert.NoError(t, err)
+
+	clock := clock.NewMock()
+
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	mm := mock.NewMockMeter(ctrl)
+	me := mock.NewMockMeterEnergy(ctrl)
+
+	type EnergyDecorator struct {
+		api.Meter
+		api.MeterEnergy
+	}
+
+	cm := &EnergyDecorator{Meter: mm, MeterEnergy: me}
+
+	lp := &Loadpoint{
+		clock:       clock,
+		db:          db,
+		chargeMeter: cm,
+	}
+
+	// create session
+	me.EXPECT().TotalEnergy().Return(1.0, nil)
+	lp.createSession()
+	assert.NotNil(t, lp.session)
+
+	// start charging
+	lp.updateSession(func(session *coredb.Session) {
+		if session.Created.IsZero() {
+			session.Created = lp.clock.Now()
+		}
+	})
+	assert.Equal(t, clock.Now(), lp.session.Created)
+
+	// stop charging
+	clock.Add(time.Hour)
+	lp.chargedEnergy = 1.23 * 1e3                                   // Wh
+	me.EXPECT().TotalEnergy().Return(1.0+lp.chargedEnergy/1e3, nil) // match chargedEnergy
+
+	lp.stopSession()
+	assert.NotNil(t, lp.session)
+	assert.Equal(t, lp.chargedEnergy/1e3, lp.session.ChargedEnergy)
+	assert.Equal(t, clock.Now(), lp.session.Finished)
+
+	s, err := db.Sessions()
+	assert.NoError(t, err)
+	assert.Len(t, s, 1)
+	t.Log(s)
+
+	// stop charging - 2nd leg
+	clock.Add(time.Hour)
+	lp.chargedEnergy *= 2
+	me.EXPECT().TotalEnergy().Return(3.0, nil) // doesn't match chargedEnergy
+
+	lp.stopSession()
+	assert.NotNil(t, lp.session)
+	assert.Equal(t, 3.0-1.0, lp.session.ChargedEnergy) // expect actual meter energy delta
+	assert.Equal(t, clock.Now(), lp.session.Finished)
+
+	s, err = db.Sessions()
+	assert.NoError(t, err)
+	assert.Len(t, s, 1)
+	t.Log(s)
+}
diff --git a/provider/http_test.go b/provider/http_test.go
new file mode 100644
index 00000000..d8e0b661
--- /dev/null
+++ b/provider/http_test.go
@@ -0,0 +1,55 @@
+package provider
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"testing"
+
+	"github.com/evcc-io/evcc/util"
+	"github.com/samber/lo"
+	"github.com/stretchr/testify/assert"
+)
+
+type httpHandler struct {
+	val string
+	req *http.Request
+}
+
+func (h *httpHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	h.req = req
+	h.val = lo.RandomString(16, lo.LettersCharset)
+	_, _ = w.Write([]byte(h.val))
+}
+
+func TestHttpGet(t *testing.T) {
+	h := new(httpHandler)
+	srv := httptest.NewServer(h)
+	defer srv.Close()
+
+	uri := srv.URL + "/foo/bar"
+	p := NewHTTP(util.NewLogger("foo"), http.MethodGet, uri, false, 1, 0)
+
+	uriUrl, _ := url.Parse(uri)
+
+	res, err := p.StringGetter()()
+	assert.NoError(t, err)
+	assert.Equal(t, uriUrl.Path, h.req.URL.Path)
+	assert.Equal(t, h.val, res)
+}
+
+func TestHttpSet(t *testing.T) {
+	h := new(httpHandler)
+	srv := httptest.NewServer(h)
+	defer srv.Close()
+
+	uri := srv.URL + "/foo/bar?baz={{.baz}}"
+	p := NewHTTP(util.NewLogger("foo"), http.MethodGet, uri, false, 1, 0)
+
+	uriUrl, _ := url.Parse(uri)
+
+	err := p.StringSetter("baz")("4711")
+	assert.NoError(t, err)
+	assert.Equal(t, uriUrl.Path, h.req.URL.Path)
+	assert.Equal(t, "baz=4711", h.req.URL.RawQuery)
+}