Add per-consumer accepted import tolerance for wallboxes
With a 4.6kW inverter and 4kW WallboxB + ~450W house load, any PV dip causes brief grid import and would trigger shutdown/restart cycling. New config: wallbox_a_accepted_import_w / wallbox_b_accepted_import_w WallboxB set to 600W: engine skips shutdown if import <= 600W, keeping the car charging through short cloud shadows. WallboxA disabled (2kW leaves sufficient inverter headroom without needing tolerance). Logic: shutdownLastConsumer checks per-consumer tolerance before applying min-runtime and initiating turn-off. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -81,6 +81,8 @@ consumers:
|
|||||||
compressor_idle_w: 500 # heat pump compressor below this = idle (W); running starts at ~2700W, so 500W cleanly separates idle (0W) from running
|
compressor_idle_w: 500 # heat pump compressor below this = idle (W); running starts at ~2700W, so 500W cleanly separates idle (0W) from running
|
||||||
wallbox_min_charge_w: 50 # wallbox below this = car not charging (W)
|
wallbox_min_charge_w: 50 # wallbox below this = car not charging (W)
|
||||||
idle_cycles: 3 # consecutive idle cycles before early release (~6 min at 2-min poll)
|
idle_cycles: 3 # consecutive idle cycles before early release (~6 min at 2-min poll)
|
||||||
|
wallbox_a_accepted_import_w: 0 # tolerate this much grid import while WallboxA is running (0 = disabled; WallboxA is 2kW, inverter headroom is sufficient)
|
||||||
|
wallbox_b_accepted_import_w: 600 # tolerate up to 600W import while WallboxB runs (4kW wallbox + 450W house > 4.6kW inverter max on PV dips)
|
||||||
|
|
||||||
# Season detection
|
# Season detection
|
||||||
season:
|
season:
|
||||||
|
|||||||
@@ -109,9 +109,11 @@ type PowerThresholds struct {
|
|||||||
|
|
||||||
// ConsumersConfig holds per-consumer behavior thresholds.
|
// ConsumersConfig holds per-consumer behavior thresholds.
|
||||||
type ConsumersConfig struct {
|
type ConsumersConfig struct {
|
||||||
CompressorIdleW int `yaml:"compressor_idle_w"` // below this = heat pump compressor idle (W)
|
CompressorIdleW int `yaml:"compressor_idle_w"` // below this = heat pump compressor idle (W)
|
||||||
WallboxMinChargeW int `yaml:"wallbox_min_charge_w"` // below this = car not charging (W)
|
WallboxMinChargeW int `yaml:"wallbox_min_charge_w"` // below this = car not charging (W)
|
||||||
IdleCycles int `yaml:"idle_cycles"` // consecutive idle cycles before early release
|
IdleCycles int `yaml:"idle_cycles"` // consecutive idle cycles before early release
|
||||||
|
WallboxAAcceptedImportW float64 `yaml:"wallbox_a_accepted_import_w"` // tolerate this much grid import while WallboxA is running (0 = disabled)
|
||||||
|
WallboxBAcceptedImportW float64 `yaml:"wallbox_b_accepted_import_w"` // tolerate this much grid import while WallboxB is running (0 = disabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StrategicConfig holds PV forecast based strategic settings.
|
// StrategicConfig holds PV forecast based strategic settings.
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ func (e *Engine) Decide(state collector.SystemState, now time.Time, wwBoostC flo
|
|||||||
}
|
}
|
||||||
importDuration := now.Sub(e.hyst.ImportSinceAbove)
|
importDuration := now.Sub(e.hyst.ImportSinceAbove)
|
||||||
if importDuration >= e.cfg.Hysteresis.ImportOffDurationParsed() {
|
if importDuration >= e.cfg.Hysteresis.ImportOffDurationParsed() {
|
||||||
if a := e.shutdownLastConsumer(now); a != nil {
|
if a := e.shutdownLastConsumer(now, gridW); a != nil {
|
||||||
actions = append(actions, *a)
|
actions = append(actions, *a)
|
||||||
e.hyst.ImportSinceAbove = time.Time{}
|
e.hyst.ImportSinceAbove = time.Time{}
|
||||||
}
|
}
|
||||||
@@ -352,7 +352,8 @@ func (e *Engine) evaluateTurnOn(
|
|||||||
|
|
||||||
// shutdownLastConsumer turns off the lowest-priority active consumer
|
// shutdownLastConsumer turns off the lowest-priority active consumer
|
||||||
// that has exceeded its minimum runtime.
|
// that has exceeded its minimum runtime.
|
||||||
func (e *Engine) shutdownLastConsumer(now time.Time) *Action {
|
// gridW is the current grid power (positive = import) used to check per-consumer import tolerance.
|
||||||
|
func (e *Engine) shutdownLastConsumer(now time.Time, gridW float64) *Action {
|
||||||
// Reverse priority: WallboxB → WallboxA → WW → SGReady
|
// Reverse priority: WallboxB → WallboxA → WW → SGReady
|
||||||
order := []Consumer{ConsumerWallboxB, ConsumerWallboxA, ConsumerWW, ConsumerSGReady}
|
order := []Consumer{ConsumerWallboxB, ConsumerWallboxA, ConsumerWW, ConsumerSGReady}
|
||||||
|
|
||||||
@@ -371,6 +372,24 @@ func (e *Engine) shutdownLastConsumer(now time.Time) *Action {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per-consumer accepted import tolerance: if the current import is within
|
||||||
|
// the configured tolerance for this wallbox, skip shutdown.
|
||||||
|
var acceptedImportW float64
|
||||||
|
switch c {
|
||||||
|
case ConsumerWallboxA:
|
||||||
|
acceptedImportW = e.cfg.Consumers.WallboxAAcceptedImportW
|
||||||
|
case ConsumerWallboxB:
|
||||||
|
acceptedImportW = e.cfg.Consumers.WallboxBAcceptedImportW
|
||||||
|
}
|
||||||
|
if acceptedImportW > 0 && gridW <= acceptedImportW {
|
||||||
|
e.logger.Debug("skipping shutdown, import within accepted tolerance",
|
||||||
|
"consumer", c,
|
||||||
|
"grid_w", gridW,
|
||||||
|
"accepted_import_w", acceptedImportW,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
minRuntime := e.minRuntime(c)
|
minRuntime := e.minRuntime(c)
|
||||||
runtime := now.Sub(cs.ActivatedAt)
|
runtime := now.Sub(cs.ActivatedAt)
|
||||||
if runtime < minRuntime {
|
if runtime < minRuntime {
|
||||||
|
|||||||
Reference in New Issue
Block a user