Heating: add daily session reset (#22068)

This commit is contained in:
andig 2025-06-26 16:47:01 +02:00 • committed by GitHub
parent feba941091
commit d91690e463
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 140 additions and 8 deletions

View file

@ -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 {

View file

@ -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

View file

@ -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)

View file

@ -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()
}

View file

@ -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)
}