Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion core/loadpoint_vehicle.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,14 @@ func (lp *Loadpoint) setActiveVehicle(v api.Vehicle) {

// re-publish vehicle settings
lp.publish(keys.PhasesActive, lp.ActivePhases())
lp.unpublishVehicle()

// only reset published vehicle data when the active vehicle actually changed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we keep comments to 2 lines please? Your AI ignores the agents.md?

// vehicleDefaultOrDetect re-assigns the same default vehicle on every reconnect
// to refresh the soc estimator (#27359); zeroing a still-valid soc there makes
// vehicleSoc/vehicleRange flap to 0 while charging until the next soc read.
if from != to {
lp.unpublishVehicle()
}

// publish effective values
lp.PublishEffectiveValues()
Expand Down
38 changes: 38 additions & 0 deletions core/loadpoint_vehicle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,44 @@ func TestDefaultVehicle(t *testing.T) {
assert.Nil(t, lp.vehicle, "expected no vehicle")
}

// TestReassignActiveVehicleKeepsSoc is a regression test for vehicleSoc dropping to
// 0 while charging. vehicleDefaultOrDetect re-assigns the already-active default
// vehicle on every (re)connect so the soc estimator is refreshed for the new session
// (#27359, #27364). setActiveVehicle unconditionally calls unpublishVehicle, which
// zeroes vehicleSoc and publishes 0. When the active vehicle does not actually
// change, a known soc must be preserved - otherwise /api/state and messaging show 0
// until the next successful soc read, which manifests as the soc flapping to 0 while
// charging after an evcc restart mid-session.
func TestReassignActiveVehicleKeepsSoc(t *testing.T) {
ctrl := gomock.NewController(t)

v := api.NewMockVehicle(ctrl)
v.EXPECT().GetTitle().Return("default").AnyTimes()
v.EXPECT().Icon().Return("").AnyTimes()
v.EXPECT().Capacity().AnyTimes()
v.EXPECT().Phases().AnyTimes()
v.EXPECT().OnIdentified().Return(api.ActionConfig{}).AnyTimes()

lp := NewLoadpoint(util.NewLogger("foo"), settings.NewDatabaseSettingsAdapter("foo"))
lp.defaultVehicle = v

x, y, z := createChannels(t)
attachChannels(lp, x, y, z)

// vehicle becomes active and a soc has been read and published
lp.setActiveVehicle(v)
lp.vehicleSoc = 71

// same default vehicle re-assigned (e.g. vehicleDefaultOrDetect on reconnect)
lp.setActiveVehicle(v)
assert.Equal(t, v, lp.vehicle, "expected same vehicle to stay active")
assert.Equal(t, 71.0, lp.vehicleSoc, "soc must be preserved when the active vehicle is unchanged")

// switching the active vehicle must still clear the stale soc
lp.setActiveVehicle(nil)
assert.Zero(t, lp.vehicleSoc, "soc must be cleared when the vehicle changes")
}

// integratedDeviceCharger is a minimal charger advertising the IntegratedDevice feature.
type integratedDeviceCharger struct{}

Expand Down
Loading