From bf57d398c21c1a74a075459573e570616ed0dbfd Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 22 Aug 2026 14:02:27 +0200 Subject: [PATCH] Loadpoint: project continuous device min power shortfall into PV disable gate (#33075) --- core/loadpoint.go | 9 +++++---- core/loadpoint_test.go | 21 ++++++++++++++------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/core/loadpoint.go b/core/loadpoint.go index aa12b2320..968852827 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -1657,10 +1657,11 @@ func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower, batteryPower f // notes: activePhases can be 1, 2 or 3 and phaseTimer can only be active if lp current is already at minCurrent projectedSitePower -= Voltage * minCurrent * float64(activePhases-1) } - // a continuous device that is enabled but not consuming keeps its demand out - // of site power, hiding insufficient surplus until it starts (#32282) - if lp.chargerHasFeature(api.Continuous) && !lp.charging() { - projectedSitePower += currentToPower(minCurrent, lp.minActivePhases()) + // a continuous device consuming less than its min power demand keeps the + // remainder out of site power, hiding insufficient surplus until it ramps + // up (#32282). Project the shortfall towards min power into the gate. + if lp.chargerHasFeature(api.Continuous) { + projectedSitePower += max(0, currentToPower(minCurrent, lp.minActivePhases())-lp.chargePower) } // kick off disable sequence, unless climater keep-alive is holding // charging at minCurrent — otherwise the "pausing soon" badge would diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go index 41f11aae3..9e07e2566 100644 --- a/core/loadpoint_test.go +++ b/core/loadpoint_test.go @@ -909,11 +909,12 @@ func TestNewLoadpointFromConfigDisabledVehicle(t *testing.T) { require.Empty(t, config.Instances(config.Vehicles().Devices())) } -// TestPVDisableIdleContinuousDevice is a regression test for #32282: a continuous -// device that is enabled but not consuming (heat pump boost signalled, compressor -// idle) does not show its demand in site power. Without projecting it, the disable -// gate only trips on grid consumption which the idle device never causes. -func TestPVDisableIdleContinuousDevice(t *testing.T) { +// TestPVDisableContinuousDeviceShortfall is a regression test for #32282: a continuous +// device consuming less than its min power demand keeps the remainder out of site +// power, so the disable gate never trips on the missing surplus. The shortfall +// towards min power is projected into the gate regardless of charge status, since +// some chargers (sgready) report StatusC while the device is idle. +func TestPVDisableContinuousDeviceShortfall(t *testing.T) { const dt = time.Minute tc := []struct { @@ -927,8 +928,14 @@ func TestPVDisableIdleContinuousDevice(t *testing.T) { {"idle, insufficient surplus", api.StatusB, 0, -400, 0}, // idle device, export covers its demand: keep enabled {"idle, sufficient surplus", api.StatusB, 0, -700, 7}, - // consuming device: measured power reflects demand, keep lenient hysteresis - {"consuming, exporting", api.StatusC, 300, -200, minA}, + // idle device reporting StatusC (sgready-style): still disable + {"idle, StatusC, insufficient surplus", api.StatusC, 0, -400, 0}, + // starting device: own draw plus surplus covers min power, keep enabled + {"starting, demand covered", api.StatusB, 300, -400, 7}, + // running below min power without surplus for the remaining demand: disable + {"running below min, insufficient surplus", api.StatusC, 300, -200, 0}, + // consuming min power, importing: unchanged disable behavior + {"consuming, importing", api.StatusC, 600, 100, 0}, } for _, tc := range tc {