From d91690e463ec90fdcac2cde371b97123085d155e Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 26 Jun 2025 16:47:01 +0200 Subject: [PATCH] Heating: add daily session reset (#22068) --- api/api.go | 2 +- api/mock.go | 42 +++++++++++++++- core/loadpoint.go | 2 + core/loadpoint_session.go | 15 ++++++ core/loadpoint_session_test.go | 87 ++++++++++++++++++++++++++++++++-- 5 files changed, 140 insertions(+), 8 deletions(-) diff --git a/api/api.go b/api/api.go index 0d05c52d2..c4235bb83 100644 --- a/api/api.go +++ b/api/api.go @@ -7,7 +7,7 @@ import ( "time" ) -//go:generate go tool mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,CurrentGetter,PhaseSwitcher,PhaseGetter,Identifier,Meter,MeterEnergy,PhaseCurrents,Vehicle,ChargeRater,Battery,Tariff,BatteryController,Circuit +//go:generate go tool mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,CurrentGetter,PhaseSwitcher,PhaseGetter,FeatureDescriber,Identifier,Meter,MeterEnergy,PhaseCurrents,Vehicle,ChargeRater,Battery,Tariff,BatteryController,Circuit // Meter provides total active power in W type Meter interface { diff --git a/api/mock.go b/api/mock.go index b969e77b0..7b4a28a46 100644 --- a/api/mock.go +++ b/api/mock.go @@ -1,9 +1,9 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/evcc-io/evcc/api (interfaces: Charger,ChargeState,CurrentLimiter,CurrentGetter,PhaseSwitcher,PhaseGetter,Identifier,Meter,MeterEnergy,PhaseCurrents,Vehicle,ChargeRater,Battery,Tariff,BatteryController,Circuit) +// Source: github.com/evcc-io/evcc/api (interfaces: Charger,ChargeState,CurrentLimiter,CurrentGetter,PhaseSwitcher,PhaseGetter,FeatureDescriber,Identifier,Meter,MeterEnergy,PhaseCurrents,Vehicle,ChargeRater,Battery,Tariff,BatteryController,Circuit) // // Generated by this command: // -// mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,CurrentGetter,PhaseSwitcher,PhaseGetter,Identifier,Meter,MeterEnergy,PhaseCurrents,Vehicle,ChargeRater,Battery,Tariff,BatteryController,Circuit +// mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,CurrentGetter,PhaseSwitcher,PhaseGetter,FeatureDescriber,Identifier,Meter,MeterEnergy,PhaseCurrents,Vehicle,ChargeRater,Battery,Tariff,BatteryController,Circuit // // Package api is a generated GoMock package. @@ -292,6 +292,44 @@ func (mr *MockPhaseGetterMockRecorder) GetPhases() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPhases", reflect.TypeOf((*MockPhaseGetter)(nil).GetPhases)) } +// MockFeatureDescriber is a mock of FeatureDescriber interface. +type MockFeatureDescriber struct { + ctrl *gomock.Controller + recorder *MockFeatureDescriberMockRecorder + isgomock struct{} +} + +// MockFeatureDescriberMockRecorder is the mock recorder for MockFeatureDescriber. +type MockFeatureDescriberMockRecorder struct { + mock *MockFeatureDescriber +} + +// NewMockFeatureDescriber creates a new mock instance. +func NewMockFeatureDescriber(ctrl *gomock.Controller) *MockFeatureDescriber { + mock := &MockFeatureDescriber{ctrl: ctrl} + mock.recorder = &MockFeatureDescriberMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockFeatureDescriber) EXPECT() *MockFeatureDescriberMockRecorder { + return m.recorder +} + +// Features mocks base method. +func (m *MockFeatureDescriber) Features() []Feature { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Features") + ret0, _ := ret[0].([]Feature) + return ret0 +} + +// Features indicates an expected call of Features. +func (mr *MockFeatureDescriberMockRecorder) Features() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Features", reflect.TypeOf((*MockFeatureDescriber)(nil).Features)) +} + // MockIdentifier is a mock of Identifier interface. type MockIdentifier struct { ctrl *gomock.Controller diff --git a/core/loadpoint.go b/core/loadpoint.go index 429efbe42..6ef271d2d 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -1820,6 +1820,8 @@ func (lp *Loadpoint) Update(sitePower, batteryBoostPower float64, rates api.Rate lp.publish(keys.Connected, lp.connected()) lp.publish(keys.Charging, lp.charging()) + lp.resetHeatingSession() + if sr, ok := lp.charger.(api.StatusReasoner); ok && lp.GetStatus() == api.StatusB { if r, err := sr.StatusReason(); err == nil { lp.publish(keys.ChargerStatusReason, r) diff --git a/core/loadpoint_session.go b/core/loadpoint_session.go index 77b78df62..f6decb653 100644 --- a/core/loadpoint_session.go +++ b/core/loadpoint_session.go @@ -3,6 +3,7 @@ package core import ( "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/core/session" + "github.com/jinzhu/now" "github.com/samber/lo" ) @@ -106,3 +107,17 @@ func (lp *Loadpoint) clearSession() { lp.session = nil } + +func (lp *Loadpoint) resetHeatingSession() { + if lp.session == nil || !lp.chargerHasFeature(api.Heating) || !lp.chargerHasFeature(api.IntegratedDevice) { + return + } + + if !now.With(lp.clock.Now()).BeginningOfDay().After(lp.session.Created) { + return + } + + lp.stopSession() + lp.clearSession() + lp.createSession() +} diff --git a/core/loadpoint_session_test.go b/core/loadpoint_session_test.go index 946c6df72..a6f833ec1 100644 --- a/core/loadpoint_session_test.go +++ b/core/loadpoint_session_test.go @@ -14,6 +14,14 @@ import ( "go.uber.org/mock/gomock" ) +func sessionStart(lp *Loadpoint) func(session *session.Session) { + return func(session *session.Session) { + if session.Created.IsZero() { + session.Created = lp.clock.Now() + } + } +} + func TestSession(t *testing.T) { var err error serverdb.Instance, err = serverdb.New("sqlite", ":memory:") @@ -50,11 +58,7 @@ func TestSession(t *testing.T) { assert.NotNil(t, lp.session) // start charging - lp.updateSession(func(session *session.Session) { - if session.Created.IsZero() { - session.Created = lp.clock.Now() - } - }) + lp.updateSession(sessionStart(lp)) assert.Equal(t, clock.Now(), lp.session.Created) // stop charging @@ -181,3 +185,76 @@ func createMockSessions(db *session.DB, clock *clock.Mock) []*session.Session { } return sessions } + +func TestResetHeatingSession(t *testing.T) { + var err error + serverdb.Instance, err = serverdb.New("sqlite", ":memory:") + require.NoError(t, err) + + db, err := session.NewStore("foo", serverdb.Instance) + require.NoError(t, err) + + clock := clock.NewMock() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cc := api.NewMockCharger(ctrl) + fd := api.NewMockFeatureDescriber(ctrl) + + type FeatureDecorator struct { + api.Charger + api.FeatureDescriber + } + + charger := &FeatureDecorator{Charger: cc, FeatureDescriber: fd} + fd.EXPECT().Features().AnyTimes().Return([]api.Feature{ + api.Heating, api.IntegratedDevice, + }) + + mm := api.NewMockMeter(ctrl) + me := api.NewMockMeterEnergy(ctrl) + + type EnergyDecorator struct { + api.Meter + api.MeterEnergy + } + + cm := &EnergyDecorator{Meter: mm, MeterEnergy: me} + + lp := &Loadpoint{ + log: util.NewLogger("foo"), + clock: clock, + db: db, + charger: charger, + chargeMeter: cm, + } + + // create session + me.EXPECT().TotalEnergy().Return(1.0, nil) + lp.createSession() + require.NotNil(t, lp.session) + assert.True(t, lp.session.Created.IsZero()) + + // actually mark session as started + lp.session.Created = clock.Now() + assert.Equal(t, clock.Now(), lp.session.Created) + + clock.Add(36 * time.Hour) + me.EXPECT().TotalEnergy().Return(1.0, nil).MaxTimes(2) + + lp.resetHeatingSession() + require.NotNil(t, lp.session) + assert.True(t, lp.session.Created.IsZero()) + + lp.updateSession(sessionStart(lp)) + assert.Equal(t, clock.Now(), lp.session.Created) + + me.EXPECT().TotalEnergy().Return(3.0, nil) + lp.stopSession() + + assert.NotNil(t, lp.session) + assert.Equal(t, clock.Now(), lp.session.Finished) + assert.Equal(t, 1.0, *lp.session.MeterStart) + assert.Equal(t, 3.0, *lp.session.MeterStop) +}