You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Random Predbat exports, often in the peak rate #4840
I am on Octopus Cosy (cheap rate 1-4pm 10.54p) and the fixed 12p Octopus export tariff.
So other than when I have a Power up period coming up or a Power down/Axle event, Predbat should not ordinarily be exporting as the difference between import and export rate makes it not worthwhile churning the batteries.
Yet randomly I see Predbat executing an export, quite often in the peak rate which could have an impact on the export baseline for future power down sessions.
An example yesterday at 17:06, Predbat had previously been in freeze export mode, the batteries being sufficiently charged for overnight, and all solar being exported. At 17:06 Predbat started an export which I cancelled with a manual freeze export override at 17:18.
I don't believe there's any good reason why Predbat was doing this export, the previous plan didn't have any export in it.
Expected behaviour
No random exports when rates don't make it beneficial.
Likely duplicate/recurrence of #4453. Same reporter, same hardware (2x GivEnergy Gen1 hybrid + 1x AC3), same symptom ("random export, often in the peak rate", flat 12p export tariff). #4453 was last updated 2026-08-21 with a comment that the symptom "appears to have been resolved" after freeze-export modelling work (#4207/#4582) — but it was never closed, and this report on v8.53.5 (8 days later) shows it recurring. Worth linking/tracking against that thread rather than treating as fully independent.
Investigated against current main (v8.53.5-194-g112b7cf4, 194 commits ahead of the reporter's version):
Replayed the attached debug_history_snapshot_20260828-170000.txt against current main with ./run_all --debug_file ... --redo — it reproduces exactly: the recomputed plan metric (-937.5024) matches what the reporter's own log shows at that timestamp, so this is not something already fixed on main.
Traced the exact flip in the reporter's log around 17:05:
At 17:00 and the start of the 17:05 cycle, the held plan is Freeze Export as exporting is now at/below target (current SoC 18.9kWh, target 29.8kWh) — correctly not exporting.
Mid-cycle, plan.py's periodic full recompute fires ("Will recompute the plan as it is now 10.0 minutes old", driven by calculate_plan_every vs RUN_EVERY). A fresh, non-exhaustive region+detail search produces a different local optimum for the immediate window: an active export target of 15.36kWh (56%), i.e. force-discharging the battery to sell at the flat 12p export rate right as the Cosy peak import band (39.5p) begins.
should_replace_plan() (plan.py:1175-1188) compares the two plans: "Previous plan best metric is -959.74 ... new plan best metric is -962.15" — a 2.41p improvement over the 48h horizon, just over the default metric_min_improvement_plan (2.0p, confirmed unchanged in the reporter's config) anti-jitter threshold, so the new plan replaces the freeze incumbent.
This lines up with the finding already posted on Predbat randomly exports instead of freeze export, often in the peak rate #4453 by @chalfontchubby: on a flat/near-flat export tariff, many charge/export allocations score within a few pence of each other over a 48h plan, so the non-exhaustive search can land on a different (but not obviously "wrong" by Predbat's own cost model) local optimum between one recompute and the next, and a small crossing of metric_min_improvement_plan is enough to adopt it. That earlier investigation could not find anything mis-set in that reporter's config either.
Nothing in this reporter's non-default config looks like an obvious misconfiguration (metric_min_improvement, metric_min_improvement_export, metric_min_improvement_export_freeze are all raised above default, which if anything should make the optimiser more conservative, not less).
Root cause: not pinned to a single line — this looks like the search/tie-break instability class already tracked under #4453, now confirmed reproducible via a second, independently-captured debug snapshot. A maintainer familiar with the region/detail search in plan.py (optimise_all_windows, optimise_detailed_pass) would be better placed to judge whether the fix is a wider anti-jitter margin specifically for near-tied export decisions, or a determinism fix in the search itself.
No missing information — version, environment, log and debug snapshot were all provided and were sufficient to investigate.
Thanks. The infomation is all there I'm sure - but because it's presented in a different format to the "normal" pland and history, it does take more cognitive load to process it.
I'll try and generate the history/plan in a form I'm familiar with.
I've replayed your snapshot and I think Predbat is doing exactly what your settings ask of it. From the report:
Predbat should not ordinarily be exporting as the difference between import and export rate makes it not worthwhile churning the batteries.
There is a setting whose whole job is to express that — input_number.predbat_metric_battery_cycle — and it's at 0.0 in your config, so churn currently costs Predbat nothing to consider.
Exporting does two things: it lowers cost (money in now) and it lowers soc_end. Your config nudges both terms toward exporting:
metric_battery_cycle: 0.0 (default 0) — the extra kWh through the battery is free.
metric_battery_value_scaling: 0.9 (default 1.0) — the SoC you gave up is only debited at 90% of its worth, so every export looks ~10% cheaper than it is. At rate_forward 11.32p that's a manufactured ~1.1p/kWh incentive to dump, against a 12p export rate.
Cycle cost also works on the other side: battery_value_rate() adds metric_battery_cycle into the replacement cost of a stored kWh, so raising it both penalises the cycle and raises what the stored energy is worth. Both push against exporting.
A/B replay of your own snapshot
./run_all --debug_file debug_history_snapshot_20260828-170000.txt, changing one setting at a time. Baseline reproduces your log exactly (cost -937.5024, matching Marginal costs baseline metric at 17:05):
variant
17:00–17:30 window
forced exports over the 48h
cost
as configured
force export to 51%
2
−937.50p
metric_battery_cycle 0.5
gone (freeze)
6 end-of-horizon slivers
−933.35p
metric_battery_cycle 1.0
gone (freeze)
none
−885.24p
metric_battery_cycle 2.0
gone (freeze)
none
−838.42p
metric_battery_value_scaling 1.0 (cycle left at 0)
gone (freeze)
none
−885.24p
Either knob on its own removes the export you saw, and every forced export in the whole 48h plan. The docs suggest 0–2 p/kWh for cycle cost (configuration guide).
Worth noting the cost column moves ~52p in Predbat's favour with the exports — that's real modelled money over 48h, offset by ending the horizon with an emptier battery, which is precisely the trade metric_battery_value_scaling is there to price. At 0.9 you're telling it that trade is better than it is.
On your specific arithmetic
You're right that the round trip doesn't pay on Cosy: 10.54p in becomes 11.32p of replacement cost after inverter and battery losses, and 12p out comes back as 11.18p. It's a small net loss before any wear. Predbat only took the trade because it was liquidating already-stored (mostly solar-derived) energy at a 10%-discounted book value rather than round-tripping — remove the discount, or price the churn, and it stops.
Why the history view didn't show it
Not your imagination — the history reconstruction collapses each 30-minute slot to a single state, and it keeps the last matching status in the slot rather than the most active one. Your 17:18 manual freeze override overwrote the 17:05–17:17 Exporting, so the slot renders as freeze export. Raised as #4843 — the multi-inverter status resolver already has a most-active-wins precedence rule that could be reused here.
Separate, and I think more costly than the export
While tracing this I found something in the same log that looks worse than the 3.5kWh export. Through the evening, with Predbat in plain Demand and nothing forced:
time
inv0 (12.4kWh)
inv1 (9.52kWh)
inv2 (8.19kWh)
14:25
42%
100%
71%
18:20
46% ↑
97% ↑
25% ↓
19:10
54% ↑
100% ↑
4%
00:30
24%
94%
2%
At 18:20 inverter 0's battery is charging at 2424W on a PV surplus of only ~300W (1400W PV, 1120W load) — the other ~2.1kW is coming out of inverter 2's battery. By 18:50 inverter 0 is importing 1000W from the grid at 25.14p to charge itself while inverter 2 sits at 11%. That's the inverters cross-charging each other, paying round-trip losses each time, and by midnight 8.95 of the fleet's 12.30kWh is stranded in inverter 1, which barely discharges at all.
switch.predbat_balance_inverters_enable is false in your config (it is default-off and marked Beta). Its balance_inverters_crosscharge branch exists for exactly this. Might be worth a try — though I've also found a couple of gaps in the balancer itself (#4842), so it won't help during a force export as things stand.
One last thing that made this harder to read than it needed to be: the status line for the export was
Those are per-inverter SoCs against the fleet target, so two of the three segments read as though those inverters were charging up to 56%. The fleet figure is 63%-56%. Raised as #4844.
thanks for all your investigation Rik and thorough writeup.
Picking up on the points you made ....
Two knobs
metric_battery_cycle: 0.0 (default 0)
I have consciously decided to not set a metric battery cycle in order to prevent predbat from artificially constraining the plan. The inverters and batteries are all bought and paid for, there is no warranty now on the Givenergy equipment so no benefit to 'saving them', but even before the Givenergy failure I had cycle cost set to 0 so that predbat could maximise the ROI from the batteries.
When I said 'unnecessary churn on the battery', I spoke too loose. Really I should have said that predbat was exporting where there was no real financial value to do so.
metric_battery_value_scaling: 0.9 (default 1.0)
This is interesting and as you have demonstrated caused this particular export. I don't have any recollection of why I set this to 0.9 instead of the default 1.0, and clearly it distorts the plan. May also explain other similar odd behaviour of charging and then discharging as noted in #4453.
I will revert to the default setting and see if I get any reoccurrences of these random exports.
History view didn't show it
Thanks, I see the other change you are progressing. I have a comment on it, so I will log it on that change.
Cross charging
Ah yes, the bane of a multi-inverter setup.
Right from the very beginning when I started with HA and Predbat I have been trying to tackle cross charging. As you say, it occurred last night with I (green battery power line) discharging into G (yellow) and H (red)
And again tonight
When there is plenty of solar and G and H (the type hybrid inverters) are solar charging, I don't get cross charging as I (AC coupled) only charges off the excess grid export, but as the solar generation drops off and house load ramps up for cooking etc, and IF predbat is in Demand mode, the cross charging occurs.
I have tried the Predbat balance inverters and if you look back you'll find old tickets from me about how it operated. It's not at all sophisticated and Predbat basically shuts down discharge and charge rates to try to keep the inverters balanced. At the time when I saw the behaviour of this I was concerned about the impact on the inverters, but moreover it means that predbat constrained the inverters from discharging when there was genuine house load to meet. I found that I was grid importing more as a consequence.
Subsequently the whole issue of inverter EEPROM's and limited write cycles appeared, vindicating my decision to not use Predbat balancing. There is now real time registers on my inverters so the repeated charge/discharge rate setting changes would be less of an issue than it originally was, but I would still prefer not to use it.
Instead I have 'cured' most of the cross charging by adding the 'set freeze export during demand' logic, which for me is controlled by an automation that turns switch.predbat_set_freeze_export_during_demand on at 35 minutes before sunset. The sets Predbat to Freeze Export mode when it would otherwise be in Demand mode, so in other words stops the inverters from charging. No charging means no cross charging.
Looking at what has been happening the last couple of nights I should turn that control on earlier now that solar power (blue power line) is dropping off quicker. I'll change the automation to turn it on 2 hours before sunset and monitor what happens.
Thanks
Automated follow-up triage review — a maintainer will review before any labels/actions are treated as final.
New since the last bot review:@chalfontchubby's replay analysis, @gcoan's history-view screenshots, and @gcoan's reply accepting the metric_battery_value_scaling diagnosis and reverting it to 1.0.
Correction to the previous bot triage
That comment attributed this to search/tie-break instability crossing metric_min_improvement_plan. That was wrong. The outcome is deterministic and config-driven — it reproduces identically on every replay, with and without --redo. @chalfontchubby's diagnosis is correct; what follows is independent confirmation plus some new evidence.
The export rested on a forecast that collapsed 85 minutes later
The decisive point, from @gcoan's own log rather than from the replay. At 17:05 the planner predicted zero grid import for the entire 48-hour horizon (PV 105.57 kWh vs load 40.26 kWh). That is the whole economic basis for selling the battery at 12p. Tracking that same prediction forward:
17:00 import_batt=0 17:55 import_batt=0
17:35 import_batt=0 18:15 import_batt=0
18:31 import_batt=14.99 <-- 85 minutes after the export
19:10 import_batt=15.63 ... holds ~15.4 kWh all night
So Predbat sold ~3.5 kWh at 12p during a 39.5p peak import band, and by 18:31 was planning to buy 15 kWh back. The PV forecast total did not move (~106 kWh throughout) — evening load simply became real. This supports @gcoan's reframing exactly: not "churn" as wear, but exporting where there was no real financial value in doing so.
The trade loses money even at metric_battery_cycle: 0
@gcoan's reasoning for keeping cycle cost at 0 is sound, and worth saying plainly: you don't need cycle cost to prevent this. The energy arithmetic alone is negative once battery value is at its default. Replaying the snapshot and comparing the two plans:
terminal SoC
48h plan cost
as configured (exports)
1.43 kWh
−937.50p
metric_battery_value_scaling = 1.0
6.13 kWh
−885.24p
52.26p of extra cash for 4.70 kWh given up = 11.12p/kWh realised. Replacement cost on Cosy is 10.54 / 0.96 / 0.97 = 11.32p/kWh. Net −0.20p/kWh before any wear. At scaling: 0.9 that kWh is booked at 10.19p, so the same loss reads as +0.93p/kWh and the planner takes it. Reverting to 1.0 is the right fix and needs no cycle cost alongside it.
New finding: metric_cloud_enable is off, and restoring the default also prevents this
switch.predbat_metric_cloud_enable is False in the config (default True). Restoring it removes this same export:
variant
17:00-17:30 window
48h plan cost
as configured
force export to 51%
−937.50p
metric_cloud_enable = true (default)
gone
−936.33p
metric_battery_value_scaling = 1.0 (default)
gone
−885.24p
metric_battery_cycle = 1.0
gone
−885.24p
metric_pv_calibration_enable = true (default)
still exports
−937.50p
export_more_solar = false (default)
still exports
−937.50p
Identical with and without --redo. The cloud modulation is deterministic (fetch.py:200-210 — a strict alternating on/off, no RNG), so this is signal rather than luck: it perturbs PV within each period so the planner stops assuming solar exactly blankets load, which is precisely the assumption that failed at 18:31. Worth enabling alongside the metric_battery_value_scaling revert, as it addresses the fragility rather than only the mispricing. (The −936.33 vs −937.50 figures are under different PV models, so treat that gap as indicative, not a clean delta.)
Possible follow-up: the slider cannot be nudged
The margin protecting against value-free export here is 11.174 / 11.319 — 1.28%. metric_battery_value_scaling has "step": 0.1 (config.py:511-521), so the smallest possible reduction below default overshoots the flip point by 7.8x. That likely explains why 0.9 was set at some point with no obvious ill effect until a near-parity tariff came along, and docs/customisation.md:159 carries no warning. A 0.01 step and/or a doc note may be worth a separate issue — flagging rather than raising it, as it is not what this issue reports.
Checked and cleared
best_soc_keep: 0.0 and metric_battery_value_export_scaling: 0.8 are both defaults, not deviations. export_more_solar_threshold: 10.0 is a genuine non-default (10x default) and across the log accepted 25 of 150 plans that were strictly worse (worst single 8.82p) — but it did not cause this export (0 force exports re-optimised at 17:05), so it is a separate observation.
Assessment change
Reclassifying bug → configuration and priority_medium → priority_low: the root cause is confirmed configuration, reproduced three independent ways, with a one-setting fix the reporter has already applied. Happy to see this revisited if the exports recur at default metric_battery_value_scaling — in that case a fresh debug snapshot from after the change would be the thing to attach.
No information is missing; the version, environment, log and debug snapshot were all sufficient.
I would argue that while the battery is paid for and the warranty is - erm - toast, the remaining life expectancy still has value, and the cycle cost can help preserve that. If you are happy cycling whenever non-replacement finance indicates it's beneficial, fine. But I'd be trying to make it last longer personally on top of that rather than run it into the ground.
Automated follow-up triage review — a maintainer will review before any labels/actions are treated as final.
New since the last bot review:@chalfontchubby's correction to the history-view theory (root-caused the mirrored-slot bug instead, filed as #4843, and left the original "why didn't the 17:18 event show" question open pending more data); @gcoan's reply explaining the metric_cloud_enable/metric_pv_calibration_enable choices and confirming the metric_battery_value_scaling revert to default 1.0; and then @gcoan reporting a fresh recurrence today (2026-08-30), on the corrected config, with new debug snapshots and logs (09:30 cache, 12:35 live debug+log, 12:50/12:55 after a manual freeze-export override, 13:00 after the plan flipped again to Demand).
The metric_battery_value_scaling fix is confirmed in place — and the export still happened
Checked the 12:35 debug snapshot: metric_battery_value_scaling: 1.0 (default), metric_battery_cycle: 0.0 (as before, by choice) — the previously diagnosed misconfiguration is genuinely gone from this config. Yet at 12:31:06 the fleet status flips from Freeze exporting to Exporting target 50%-52% / 100%-52% / 7%-52%. So this is a different trigger than the one already fixed.
What actually flipped it: a 2.43p margin over a 48h plan worth >1100p
Tracing the 12:30:00 recompute in the reporter's log:
Previous plan best metric is -1161.65 (cost -1096.54) and new plan best metric is -1164.08 (cost -1153.9)
New plan metric is significantly better from previous plan, using new plan
That's a 2.43p improvement over the whole 48h horizon — just 0.43p over the metric_min_improvement_plan anti-jitter threshold — even though the underlying cash delta is 57.36p (cost -1096.54 → -1153.9). With the value-scaling bug now fixed, the SoC-value term absorbs almost all of that cash gain, leaving a razor-thin net edge that's well within normal forecast noise. The gate is should_replace_plan() at plan.py:1175-1186, comparing metric_prev - metric_new against self.metric_min_improvement_plan.
The reporter raised the export-specific thresholds but not this one. In the same debug snapshot: metric_min_improvement 0→6.0, metric_min_improvement_export 0.1→1.0, metric_min_improvement_export_freeze 0.1→1.0 — all raised well above default for stability, exactly as @gcoan described ("part of the reason I increased some of the metric_min_improvement figures"). But metric_min_improvement_plan is still at its default 2.0p, and that's specifically the gate that governs whether a periodic full recompute (every calculate_plan_every, 10 minutes here) is allowed to replace the entire incumbent charge/export schedule — which is exactly what happened at 12:30.
This reproduces the mechanism the first bot triage on this issue flagged, then set aside
That first pass attributed the original report to "search/tie-break instability... crossing metric_min_improvement_plan" and linked it to #4453; the follow-up review corrected that to the metric_battery_value_scaling misconfig instead. Both were right about their own event — but today's independent recurrence, under the now-corrected default scaling, shows the near-threshold whole-plan-replacement flip is a real, separate, still-live mechanism, not fully explained by the scaling fix. It's the same class of behaviour tracked under #4453 (small metric swings on a near-tied export/freeze decision flipping the fleet as the rolling 48h forecast updates).
Secondary observation, not chased further
Between 12:46 and 12:54, the log shows repeated Inverter 1 REST failed to setBatteryMode / setBatteryPauseMode got Disabled warnings roughly every 70 seconds — Inverter 1 wasn't accepting Predbat's mode-switch commands during this window. No link established to the plan-metric flip itself; flagging only in case it's relevant to why the manual override felt slow to take hold. Not investigated further as it's outside what this issue is asking about.
Assessment: unchanged
Classification stays configuration, priority stays priority_low. This is the same shape of gap as before — a tuning threshold left at default while adjacent ones were raised — rather than a code defect. If @gcoan wants to test whether this specific mechanism is the whole story, raising metric_min_improvement_plan (e.g. to 5–10p, consistent with the other thresholds already raised) and watching for further recurrences would confirm it. Given this is the second independent reproduction of the "near-tied plan swap" pattern on this account, it may be worth the maintainers considering whether metric_min_improvement_plan's default is well-matched to plans of this total magnitude — flagging for judgement, not asserting a fix.
No information is missing; the new debug snapshots and logs were sufficient for this review.
Recurrence on v8.54.2 — overnight arbitrage export during IOG charge window, despite combine_charge/calculate_export_during_charge correctly set
This bug appeared resolved after updating to v8.48.8 (clean HoldChrg→ held at 100% through the full cheap window, no mid-window export — see plan A below). It has now recurred on v8.54.2, reproduced on two separate nights (plan B, plan C).
Environment
Predbat v8.54.2 (regression from confirmed-good behaviour on v8.48.8)
Fox ESS H1-G2-6.0-Wl inverter, 4x ECS2900 batteries
Octopus Intelligent Octopus Go (IOG), flat 6.90p import / 12.00p export
combine_charge: true — confirmed set on all runs below
calculate_export_during_charge: false — confirmed set on all runs below
Ruled out
switch.predbat_export_more_solar (new in v8.54.2, suggested by maintainers) — toggled off, Predbat fully restarted, fresh plan recalculated — no change in behaviour. Not the cause.
Plan A — v8.48.8, confirmed correct behaviour:
Tue 00:00 6.90 12.00 HoldChrg→ 100 ... SoC 100→
...continuous HoldChrg→ at 100% through entire cheap window, no export
Both v8.54.2 runs show the identical pattern: charge climbs to ~96% then reverses to export down to ~42% at the flat 12p rate, arbitraging against the 6.9p import rate — exactly the behaviour calculate_export_during_charge: false should prevent. This leaves the battery badly depleted right as the 06:30-08:00 peak-rate (30.27p), low-solar window begins, forcing expensive grid import shortly after selling the cheap charge.
Since settings are unchanged between the confirmed-good and reproducing runs, and export_more_solar has been ruled out, this looks like a genuine regression in the charge-window/arbitrage logic between v8.48.8 and v8.54.2.
Can we get debug yaml files taken from the plan history before that happened please along with the log.
I can't do much with snippets of text from a plan.
This doesn't sound like an export in peak rate so doesn't feel like it's related to the issue as reported?
It's very odd - history plan is good (and I suspect debug files attached will confirm that) but battery was at 20% this morning. Live plan showed the same thing planned for tonight/tomorrow morning unbtil 08:00 then it corrcetd itself...........
Reporrt was Claude generated based on what actually (didn't) happen(ed)- i.e. start the day with a full charge at 05:30 (usual) Vs. battery depleted to 20% SoC (happened today)
Sorry - have you got the log too. I'm not sure this is anything to do with the issue this is about so is perhaps it;s own ticket - trying to keep different things separate rather than lump lots of different "it's not working" into one pot.
I'd suggest posting all of that - with the debugs and the log in as a new issue and letting the bot take a look - that will get a faster turn around than waiting for one of us - and perhaps keep this ticket focused on the exports in peak rate which might be fixed.
I'm going to close this as my issues appear to have been resolved by configuraiton changes advised above, for other people with similar issues please raise a new report
Activity
springfall2008 commentedon Aug 29, 2026
Automated first-pass triage — a maintainer will review before any labels/actions are treated as final.
Classification: bug
Priority: priority_medium (affects any flat-export-tariff user, workaround exists — manual freeze-export override)
Likely duplicate/recurrence of #4453. Same reporter, same hardware (2x GivEnergy Gen1 hybrid + 1x AC3), same symptom ("random export, often in the peak rate", flat 12p export tariff). #4453 was last updated 2026-08-21 with a comment that the symptom "appears to have been resolved" after freeze-export modelling work (#4207/#4582) — but it was never closed, and this report on v8.53.5 (8 days later) shows it recurring. Worth linking/tracking against that thread rather than treating as fully independent.
Investigated against current main (
v8.53.5-194-g112b7cf4, 194 commits ahead of the reporter's version):debug_history_snapshot_20260828-170000.txtagainst current main with./run_all --debug_file ... --redo— it reproduces exactly: the recomputed plan metric (-937.5024) matches what the reporter's own log shows at that timestamp, so this is not something already fixed on main.Freeze Export as exporting is now at/below target(current SoC 18.9kWh, target 29.8kWh) — correctly not exporting.plan.py's periodic full recompute fires ("Will recompute the plan as it is now 10.0 minutes old", driven bycalculate_plan_everyvsRUN_EVERY). A fresh, non-exhaustive region+detail search produces a different local optimum for the immediate window: an active export target of 15.36kWh (56%), i.e. force-discharging the battery to sell at the flat 12p export rate right as the Cosy peak import band (39.5p) begins.should_replace_plan()(plan.py:1175-1188) compares the two plans:"Previous plan best metric is -959.74 ... new plan best metric is -962.15"— a 2.41p improvement over the 48h horizon, just over the defaultmetric_min_improvement_plan(2.0p, confirmed unchanged in the reporter's config) anti-jitter threshold, so the new plan replaces the freeze incumbent.metric_min_improvement_planis enough to adopt it. That earlier investigation could not find anything mis-set in that reporter's config either.metric_min_improvement,metric_min_improvement_export,metric_min_improvement_export_freezeare all raised above default, which if anything should make the optimiser more conservative, not less).Root cause: not pinned to a single line — this looks like the search/tie-break instability class already tracked under #4453, now confirmed reproducible via a second, independently-captured debug snapshot. A maintainer familiar with the region/detail search in
plan.py(optimise_all_windows,optimise_detailed_pass) would be better placed to judge whether the fix is a wider anti-jitter margin specifically for near-tied export decisions, or a determinism fix in the search itself.No missing information — version, environment, log and debug snapshot were all provided and were sufficient to investigate.
chalfontchubby commentedon Aug 29, 2026
Thanks. The infomation is all there I'm sure - but because it's presented in a different format to the "normal" pland and history, it does take more cognitive load to process it.
I'll try and generate the history/plan in a form I'm familiar with.
gcoan commentedon Aug 29, 2026
Here's the history plan view from the predbat web console. Doesn't show the export though
chalfontchubby commentedon Aug 29, 2026
I've replayed your snapshot and I think Predbat is doing exactly what your settings ask of it. From the report:
There is a setting whose whole job is to express that —
input_number.predbat_metric_battery_cycle— and it's at0.0in your config, so churn currently costs Predbat nothing to consider.The two knobs
The planner's metric is, in
compute_metric():Exporting does two things: it lowers
cost(money in now) and it lowerssoc_end. Your config nudges both terms toward exporting:metric_battery_cycle: 0.0(default 0) — the extra kWh through the battery is free.metric_battery_value_scaling: 0.9(default 1.0) — the SoC you gave up is only debited at 90% of its worth, so every export looks ~10% cheaper than it is. Atrate_forward 11.32pthat's a manufactured ~1.1p/kWh incentive to dump, against a 12p export rate.Cycle cost also works on the other side:
battery_value_rate()addsmetric_battery_cycleinto the replacement cost of a stored kWh, so raising it both penalises the cycle and raises what the stored energy is worth. Both push against exporting.A/B replay of your own snapshot
./run_all --debug_file debug_history_snapshot_20260828-170000.txt, changing one setting at a time. Baseline reproduces your log exactly (cost-937.5024, matchingMarginal costs baseline metricat 17:05):metric_battery_cycle0.5metric_battery_cycle1.0metric_battery_cycle2.0metric_battery_value_scaling1.0 (cycle left at 0)Either knob on its own removes the export you saw, and every forced export in the whole 48h plan. The docs suggest 0–2 p/kWh for cycle cost (configuration guide).
Worth noting the cost column moves ~52p in Predbat's favour with the exports — that's real modelled money over 48h, offset by ending the horizon with an emptier battery, which is precisely the trade
metric_battery_value_scalingis there to price. At 0.9 you're telling it that trade is better than it is.On your specific arithmetic
You're right that the round trip doesn't pay on Cosy: 10.54p in becomes 11.32p of replacement cost after inverter and battery losses, and 12p out comes back as 11.18p. It's a small net loss before any wear. Predbat only took the trade because it was liquidating already-stored (mostly solar-derived) energy at a 10%-discounted book value rather than round-tripping — remove the discount, or price the churn, and it stops.
Why the history view didn't show it
Not your imagination — the history reconstruction collapses each 30-minute slot to a single state, and it keeps the last matching status in the slot rather than the most active one. Your 17:18 manual freeze override overwrote the 17:05–17:17
Exporting, so the slot renders as freeze export. Raised as #4843 — the multi-inverter status resolver already has a most-active-wins precedence rule that could be reused here.Separate, and I think more costly than the export
While tracing this I found something in the same log that looks worse than the 3.5kWh export. Through the evening, with Predbat in plain
Demandand nothing forced:At 18:20 inverter 0's battery is charging at 2424W on a PV surplus of only ~300W (1400W PV, 1120W load) — the other ~2.1kW is coming out of inverter 2's battery. By 18:50 inverter 0 is importing 1000W from the grid at 25.14p to charge itself while inverter 2 sits at 11%. That's the inverters cross-charging each other, paying round-trip losses each time, and by midnight 8.95 of the fleet's 12.30kWh is stranded in inverter 1, which barely discharges at all.
switch.predbat_balance_inverters_enableisfalsein your config (it is default-off and marked Beta). Itsbalance_inverters_crosschargebranch exists for exactly this. Might be worth a try — though I've also found a couple of gaps in the balancer itself (#4842), so it won't help during a force export as things stand.One last thing that made this harder to read than it needed to be: the status line for the export was
Those are per-inverter SoCs against the fleet target, so two of the three segments read as though those inverters were charging up to 56%. The fleet figure is 63%-56%. Raised as #4844.
chalfontchubby commentedon Aug 29, 2026
Filed #4843 to track the "history doesn't show the export" issue
gcoan commentedon Aug 29, 2026
thanks for all your investigation Rik and thorough writeup.
Picking up on the points you made ....
Two knobs
I have consciously decided to not set a metric battery cycle in order to prevent predbat from artificially constraining the plan. The inverters and batteries are all bought and paid for, there is no warranty now on the Givenergy equipment so no benefit to 'saving them', but even before the Givenergy failure I had cycle cost set to 0 so that predbat could maximise the ROI from the batteries.
When I said 'unnecessary churn on the battery', I spoke too loose. Really I should have said that predbat was exporting where there was no real financial value to do so.
This is interesting and as you have demonstrated caused this particular export. I don't have any recollection of why I set this to 0.9 instead of the default 1.0, and clearly it distorts the plan. May also explain other similar odd behaviour of charging and then discharging as noted in #4453.
I will revert to the default setting and see if I get any reoccurrences of these random exports.
History view didn't show it
Thanks, I see the other change you are progressing. I have a comment on it, so I will log it on that change.
Cross charging
Ah yes, the bane of a multi-inverter setup.
Right from the very beginning when I started with HA and Predbat I have been trying to tackle cross charging. As you say, it occurred last night with I (green battery power line) discharging into G (yellow) and H (red)
And again tonight
When there is plenty of solar and G and H (the type hybrid inverters) are solar charging, I don't get cross charging as I (AC coupled) only charges off the excess grid export, but as the solar generation drops off and house load ramps up for cooking etc, and IF predbat is in Demand mode, the cross charging occurs.
I have tried the Predbat balance inverters and if you look back you'll find old tickets from me about how it operated. It's not at all sophisticated and Predbat basically shuts down discharge and charge rates to try to keep the inverters balanced. At the time when I saw the behaviour of this I was concerned about the impact on the inverters, but moreover it means that predbat constrained the inverters from discharging when there was genuine house load to meet. I found that I was grid importing more as a consequence.
Subsequently the whole issue of inverter EEPROM's and limited write cycles appeared, vindicating my decision to not use Predbat balancing. There is now real time registers on my inverters so the repeated charge/discharge rate setting changes would be less of an issue than it originally was, but I would still prefer not to use it.
Instead I have 'cured' most of the cross charging by adding the 'set freeze export during demand' logic, which for me is controlled by an automation that turns switch.predbat_set_freeze_export_during_demand on at 35 minutes before sunset. The sets Predbat to Freeze Export mode when it would otherwise be in Demand mode, so in other words stops the inverters from charging. No charging means no cross charging.
Looking at what has been happening the last couple of nights I should turn that control on earlier now that solar power (blue power line) is dropping off quicker. I'll change the automation to turn it on 2 hours before sunset and monitor what happens.
Thanks
springfall2008 commentedon Aug 29, 2026
Automated follow-up triage review — a maintainer will review before any labels/actions are treated as final.
New since the last bot review: @chalfontchubby's replay analysis, @gcoan's history-view screenshots, and @gcoan's reply accepting the
metric_battery_value_scalingdiagnosis and reverting it to 1.0.Correction to the previous bot triage
That comment attributed this to search/tie-break instability crossing
metric_min_improvement_plan. That was wrong. The outcome is deterministic and config-driven — it reproduces identically on every replay, with and without--redo. @chalfontchubby's diagnosis is correct; what follows is independent confirmation plus some new evidence.The export rested on a forecast that collapsed 85 minutes later
The decisive point, from @gcoan's own log rather than from the replay. At 17:05 the planner predicted zero grid import for the entire 48-hour horizon (PV 105.57 kWh vs load 40.26 kWh). That is the whole economic basis for selling the battery at 12p. Tracking that same prediction forward:
So Predbat sold ~3.5 kWh at 12p during a 39.5p peak import band, and by 18:31 was planning to buy 15 kWh back. The PV forecast total did not move (~106 kWh throughout) — evening load simply became real. This supports @gcoan's reframing exactly: not "churn" as wear, but exporting where there was no real financial value in doing so.
The trade loses money even at
metric_battery_cycle: 0@gcoan's reasoning for keeping cycle cost at 0 is sound, and worth saying plainly: you don't need cycle cost to prevent this. The energy arithmetic alone is negative once battery value is at its default. Replaying the snapshot and comparing the two plans:
metric_battery_value_scaling= 1.052.26p of extra cash for 4.70 kWh given up = 11.12p/kWh realised. Replacement cost on Cosy is
10.54 / 0.96 / 0.97= 11.32p/kWh. Net −0.20p/kWh before any wear. Atscaling: 0.9that kWh is booked at 10.19p, so the same loss reads as +0.93p/kWh and the planner takes it. Reverting to 1.0 is the right fix and needs no cycle cost alongside it.New finding:
metric_cloud_enableis off, and restoring the default also prevents thisswitch.predbat_metric_cloud_enableis False in the config (default True). Restoring it removes this same export:metric_cloud_enable= true (default)metric_battery_value_scaling= 1.0 (default)metric_battery_cycle= 1.0metric_pv_calibration_enable= true (default)export_more_solar= false (default)Identical with and without
--redo. The cloud modulation is deterministic (fetch.py:200-210— a strict alternating on/off, no RNG), so this is signal rather than luck: it perturbs PV within each period so the planner stops assuming solar exactly blankets load, which is precisely the assumption that failed at 18:31. Worth enabling alongside themetric_battery_value_scalingrevert, as it addresses the fragility rather than only the mispricing. (The −936.33 vs −937.50 figures are under different PV models, so treat that gap as indicative, not a clean delta.)Possible follow-up: the slider cannot be nudged
The margin protecting against value-free export here is
11.174 / 11.319— 1.28%.metric_battery_value_scalinghas"step": 0.1(config.py:511-521), so the smallest possible reduction below default overshoots the flip point by 7.8x. That likely explains why 0.9 was set at some point with no obvious ill effect until a near-parity tariff came along, anddocs/customisation.md:159carries no warning. A 0.01 step and/or a doc note may be worth a separate issue — flagging rather than raising it, as it is not what this issue reports.Checked and cleared
best_soc_keep: 0.0andmetric_battery_value_export_scaling: 0.8are both defaults, not deviations.export_more_solar_threshold: 10.0is a genuine non-default (10x default) and across the log accepted 25 of 150 plans that were strictly worse (worst single 8.82p) — but it did not cause this export (0 force exports re-optimised at 17:05), so it is a separate observation.Assessment change
Reclassifying bug → configuration and priority_medium → priority_low: the root cause is confirmed configuration, reproduced three independent ways, with a one-setting fix the reporter has already applied. Happy to see this revisited if the exports recur at default
metric_battery_value_scaling— in that case a fresh debug snapshot from after the change would be the thing to attach.No information is missing; the version, environment, log and debug snapshot were all sufficient.
chalfontchubby commentedon Aug 29, 2026
I would argue that while the battery is paid for and the warranty is - erm - toast, the remaining life expectancy still has value, and the cycle cost can help preserve that. If you are happy cycling whenever non-replacement finance indicates it's beneficial, fine. But I'd be trying to make it last longer personally on top of that rather than run it into the ground.
5 remaining items
springfall2008 commentedon Aug 30, 2026
Automated follow-up triage review — a maintainer will review before any labels/actions are treated as final.
New since the last bot review: @chalfontchubby's correction to the history-view theory (root-caused the mirrored-slot bug instead, filed as #4843, and left the original "why didn't the 17:18 event show" question open pending more data); @gcoan's reply explaining the
metric_cloud_enable/metric_pv_calibration_enablechoices and confirming themetric_battery_value_scalingrevert to default 1.0; and then @gcoan reporting a fresh recurrence today (2026-08-30), on the corrected config, with new debug snapshots and logs (09:30 cache, 12:35 live debug+log, 12:50/12:55 after a manual freeze-export override, 13:00 after the plan flipped again toDemand).The
metric_battery_value_scalingfix is confirmed in place — and the export still happenedChecked the 12:35 debug snapshot:
metric_battery_value_scaling: 1.0(default),metric_battery_cycle: 0.0(as before, by choice) — the previously diagnosed misconfiguration is genuinely gone from this config. Yet at 12:31:06 the fleet status flips fromFreeze exportingtoExporting target 50%-52% / 100%-52% / 7%-52%. So this is a different trigger than the one already fixed.What actually flipped it: a 2.43p margin over a 48h plan worth >1100p
Tracing the 12:30:00 recompute in the reporter's log:
That's a 2.43p improvement over the whole 48h horizon — just 0.43p over the
metric_min_improvement_plananti-jitter threshold — even though the underlying cash delta is 57.36p (cost -1096.54 → -1153.9). With the value-scaling bug now fixed, the SoC-value term absorbs almost all of that cash gain, leaving a razor-thin net edge that's well within normal forecast noise. The gate isshould_replace_plan()atplan.py:1175-1186, comparingmetric_prev - metric_newagainstself.metric_min_improvement_plan.The reporter raised the export-specific thresholds but not this one. In the same debug snapshot:
metric_min_improvement0→6.0,metric_min_improvement_export0.1→1.0,metric_min_improvement_export_freeze0.1→1.0 — all raised well above default for stability, exactly as @gcoan described ("part of the reason I increased some of the metric_min_improvement figures"). Butmetric_min_improvement_planis still at its default 2.0p, and that's specifically the gate that governs whether a periodic full recompute (everycalculate_plan_every, 10 minutes here) is allowed to replace the entire incumbent charge/export schedule — which is exactly what happened at 12:30.This reproduces the mechanism the first bot triage on this issue flagged, then set aside
That first pass attributed the original report to "search/tie-break instability... crossing
metric_min_improvement_plan" and linked it to #4453; the follow-up review corrected that to themetric_battery_value_scalingmisconfig instead. Both were right about their own event — but today's independent recurrence, under the now-corrected default scaling, shows the near-threshold whole-plan-replacement flip is a real, separate, still-live mechanism, not fully explained by the scaling fix. It's the same class of behaviour tracked under #4453 (small metric swings on a near-tied export/freeze decision flipping the fleet as the rolling 48h forecast updates).Secondary observation, not chased further
Between 12:46 and 12:54, the log shows repeated
Inverter 1 REST failed to setBatteryMode/setBatteryPauseMode got Disabledwarnings roughly every 70 seconds — Inverter 1 wasn't accepting Predbat's mode-switch commands during this window. No link established to the plan-metric flip itself; flagging only in case it's relevant to why the manual override felt slow to take hold. Not investigated further as it's outside what this issue is asking about.Assessment: unchanged
Classification stays
configuration, priority stayspriority_low. This is the same shape of gap as before — a tuning threshold left at default while adjacent ones were raised — rather than a code defect. If @gcoan wants to test whether this specific mechanism is the whole story, raisingmetric_min_improvement_plan(e.g. to 5–10p, consistent with the other thresholds already raised) and watching for further recurrences would confirm it. Given this is the second independent reproduction of the "near-tied plan swap" pattern on this account, it may be worth the maintainers considering whethermetric_min_improvement_plan's default is well-matched to plans of this total magnitude — flagging for judgement, not asserting a fix.No information is missing; the new debug snapshots and logs were sufficient for this review.
gcoan commentedon Aug 30, 2026
Thanks for the bot investigation
Have increased 'metric_min_improvement_plan' from 2.0 to 5.0 and will monitor again
Reduce metric_battery_value_scaling step to 0.01 and document the exp…
jHamshaw commentedon Sep 2, 2026
Recurrence on v8.54.2 — overnight arbitrage export during IOG charge window, despite combine_charge/calculate_export_during_charge correctly set
This bug appeared resolved after updating to v8.48.8 (clean HoldChrg→ held at 100% through the full cheap window, no mid-window export — see plan A below). It has now recurred on v8.54.2, reproduced on two separate nights (plan B, plan C).
Environment
Predbat v8.54.2 (regression from confirmed-good behaviour on v8.48.8)
Fox ESS H1-G2-6.0-Wl inverter, 4x ECS2900 batteries
Octopus Intelligent Octopus Go (IOG), flat 6.90p import / 12.00p export
combine_charge: true — confirmed set on all runs below
calculate_export_during_charge: false — confirmed set on all runs below
Ruled out
switch.predbat_export_more_solar (new in v8.54.2, suggested by maintainers) — toggled off, Predbat fully restarted, fresh plan recalculated — no change in behaviour. Not the cause.
Plan A — v8.48.8, confirmed correct behaviour:
Tue 00:00 6.90 12.00 HoldChrg→ 100 ... SoC 100→
...continuous HoldChrg→ at 100% through entire cheap window, no export
Plan B / Plan C — v8.54.2, bug reproduced:
Thu 02:30 6.90 12.00 ... SoC 84↗
Thu 03:00 6.90 12.00 Chrg↗ Exp↘ 42 ... SoC 96↘ ← switches to export mid-window
Thu 03:30 6.90 12.00 Exp↘ 42 ... SoC 94↘
Thu 04:00 6.90 12.00 ... SoC 81↘
Thu 04:30 6.90 12.00 ... SoC 68↘
Thu 05:00 6.90 12.00 ... SoC 55↘
Thu 05:30 30.27 12.00 ... SoC 42↘ ← left exposed at peak-rate window start
Both v8.54.2 runs show the identical pattern: charge climbs to ~96% then reverses to export down to ~42% at the flat 12p rate, arbitraging against the 6.9p import rate — exactly the behaviour calculate_export_during_charge: false should prevent. This leaves the battery badly depleted right as the 06:30-08:00 peak-rate (30.27p), low-solar window begins, forcing expensive grid import shortly after selling the cheap charge.
Since settings are unchanged between the confirmed-good and reproducing runs, and export_more_solar has been ruled out, this looks like a genuine regression in the charge-window/arbitrage logic between v8.48.8 and v8.54.2.
chalfontchubby commentedon Sep 2, 2026
Can we get debug yaml files taken from the plan history before that happened please along with the log.
I can't do much with snippets of text from a plan.
This doesn't sound like an export in peak rate so doesn't feel like it's related to the issue as reported?
jHamshaw commentedon Sep 2, 2026
predbat_debug_20260902-043000.yaml
predbat_debug_20260902-013000.yaml
It's very odd - history plan is good (and I suspect debug files attached will confirm that) but battery was at 20% this morning. Live plan showed the same thing planned for tonight/tomorrow morning unbtil 08:00 then it corrcetd itself...........
Reporrt was Claude generated based on what actually (didn't) happen(ed)- i.e. start the day with a full charge at 05:30 (usual) Vs. battery depleted to 20% SoC (happened today)
chalfontchubby commentedon Sep 2, 2026
Sorry - have you got the log too. I'm not sure this is anything to do with the issue this is about so is perhaps it;s own ticket - trying to keep different things separate rather than lump lots of different "it's not working" into one pot.
I'd suggest posting all of that - with the debugs and the log in as a new issue and letting the bot take a look - that will get a faster turn around than waiting for one of us - and perhaps keep this ticket focused on the exports in peak rate which might be fixed.
jHamshaw commentedon Sep 2, 2026
OK - thanks - leave it for now - it might have been a blip - I'll monitor again tonight...... Log attached just in case....
predbat.log
gcoan commentedon Sep 2, 2026
I'm going to close this as my issues appear to have been resolved by configuraiton changes advised above, for other people with similar issues please raise a new report
chalfontchubby commentedon Sep 2, 2026
Note for @jHamshaw - I think closing this is correct, but I do think your comment points at something real - I don't want to minimise that.
A new issue would be the way to go.
I'm concerned as well that the comments I type as a human are starting to sound a lot like Claude in my head.
Show the dominant state in a mixed history slot, not the last one seen (