Skip to content

Reduce idle during power fluctuations - #1488

Open
zoic21 wants to merge 4 commits into
Zendure:masterfrom
zoic21:reduce-idle-during-power-fluctuations
Open

Reduce idle during power fluctuations#1488
zoic21 wants to merge 4 commits into
Zendure:masterfrom
zoic21:reduce-idle-during-power-fluctuations

Conversation

@zoic21

@zoic21 zoic21 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Implements #1461: during rapid grid direction flips (e.g. a slowly ramping EV charger), the battery no longer sits fully idle in the anti-hysteresis hold-off window. Instead, devices are kept at a minimal power of ±10 W (SmartMode.POWER_IDLE) so they stay in their current inverter mode and can resume full power instantly, without extra mode switches.

Flip to charging: during the 60 s hold-off window, devices that were discharging are held at +10 W instead of being stopped. If the grid flips back to export before the window expires, they ramp straight back up — no idle period, no cold start, no mode switch.
Flip to discharging: charging devices are held at -10 W for up to HOLD_TIMEOUT (300 s, the same horizon already used to arm the 60 s window), but only when the discharging devices can cover the setpoint on their own. If more power is needed, they are released immediately, exactly as before. Held devices never leave charge mode, so charging resumes with no idle period at all once the window expires.
Offgrid (SF 2400), SOC-full and SOC-empty devices keep the previous behavior. POWER_IDLE is 10 W rather than the ±1 W suggested in the issue because it must exceed POWER_TOLERANCE (5 W), otherwise the stop command at the end of a hold would be swallowed by the device-level tolerance check.
Testing
Simulated the real powerChanged/power_charge/power_discharge code (HA 2026.2, fake devices reproducing the device-level clamp/tolerance logic, 2 s update ticks) and compared this branch tick-by-tick against master, with 1 and 2 devices:

  • Charge → discharge flip (800 W demand): identical to master at every tick — the hold never delays a needed discharge, since chargers are released whenever discharging devices can't cover the setpoint.
  • Discharge → charge flip: identical timing to master, including at the expiry of the 60 s window (a one-cycle delay found during testing is fixed by re-adding parked devices to the idle list so they restart in the same update).
  • EV-charger fluctuation (±300 W every 15 s for 5 min): inverter mode switches drop from 22 to 3, MQTT commands from 33 to 24, and discharge resumes instantly at full power on each flip back (master: idle → 50 W kickstart → full power ~4 s later). Mean residual grid power slightly improves (160 W → 150 W).
  • Cost of the trade-off: at most ~10 W flowing during a hold window (≈0.17 Wh per 60 s window); total energy charged/discharged in the flip scenarios is unchanged vs master.

Fixes #1461

Other
I think it's need to be test because leave 10W inplace of idle is beter to restart faster charge/discharge but it's consume 0,0001667 kWh (about 0,167 Wh), it's small but not 0.

claude added 3 commits July 8, 2026 13:51
…ure#1461)

When the grid direction flips from export to import, the manager arms a
hold-off window (60s when flips repeat within 5 minutes) during which the
charge setpoint is forced to 0. Previously all discharging devices were
stopped for the whole window, leaving the batteries idle during short
grid fluctuations such as a slowly ramping EV charger.

Devices that were discharging are now kept at a minimal output
(SmartMode.POWER_IDLE, 10W) while the window is running. If the grid
flips back to export before the window expires, they resume discharging
immediately without an idle period or an extra inverter mode switch.
Once the window expires and real charging starts, they are stopped as
before (the hold power exceeds POWER_TOLERANCE, so the stop command is
not skipped). Offgrid and SOC-empty devices keep the previous behavior.
…endure#1461)

Mirror of the previous commit for the charging direction. When the grid
flips from charging to discharging, charging devices were stopped
immediately; when the grid flipped back within 5 minutes, the 60s
hold-off window then kept them idle before charging could resume.

Charging devices are now kept at a minimal input (POWER_IDLE, 10W) for
HOLD_TIMEOUT (300s, the same horizon used to arm the 60s window) after
the flip, as long as the discharging devices can cover the setpoint on
their own (setpoint below POWER_START or within discharge_limit).
Because they never leave charge mode, power_charge now also holds them
at minimal input during the hold-off window instead of stopping them,
and they ramp straight back up once the window expires - no idle period
and no inverter mode switch at all.

If real discharge power is needed beyond what the discharging devices
can deliver, the chargers are released (stopped) as before, making them
available for the regular idle-device start. Offgrid and SOC-full
devices keep the previous behavior. power_discharge now takes the
update time as parameter, like power_charge already did.
…e#1461)

Simulation against master showed a one-cycle (2-4s) delay when a hold
window ends: a device parked at POWER_IDLE is still classified as
discharging (or charging), so after being stopped it was not part of the
idle list and could only be restarted by the next update. Appending it
to the idle list right after the stop lets the regular idle-device start
pick it up in the same cycle, matching the timing of the previous code
exactly.
@mggevaer

Copy link
Copy Markdown

I've had this code running for some time, looking back at the HA data, at no point during the last 3 days was sensor.zendure_manager_power 10W, which is the idle power consumption to reduce switching.
history.csv

Perhaps the 10W value is too low?
I have a Solarflow 2400AC+

Also, based on the described algorithm, perhaps it would be of value to offer an option that has a bias towards charging instead of discharging.
In my case the battery is often not fully charged by the end of the day, while some energy is still exported due to the battery algorithm being biased towards discharging combined with occasional peeks during the day.
Although this might introduce issues with the battery fluctuating between 99% and 100% a lot more, which is not healthy AFAIK.

@qpaz

qpaz commented Jul 27, 2026

Copy link
Copy Markdown

This is a code reading of the current diff (head f87126e), not a test — I have not installed this branch. Flagging one thing that I think survives the change, in case it is useful before merge.

The hold-off is still re-armed on every direction flip. In power_charge the arming line only changes its constant:

-                self.charge_time = time + timedelta(seconds=2 if (time - self.charge_last).total_seconds() > 300 else 60)
+                self.charge_time = time + timedelta(seconds=2 if (time - self.charge_last).total_seconds() > SmartMode.HOLD_TIMEOUT else 60)

and in power_discharge the reset to datetime.max stays a context line. So every discharge event still rewinds the window to a full 60 s, and setpoint = 0 still applies while it runs. The charge side therefore has no upper bound on how long it can be held: if the excitation period is shorter than the window, it never expires. On our install that is not hypothetical: 31 measured flip-to-first-charge-command latencies across 12 episodes, median 71 s, 90 % of them between 60 and 80 s. Over 30 days of recorder data, 14 such episodes exported 2907 Wh of surplus while the battery sat idle with headroom. Details and the mechanism are in my comment on #1525.

One narrower point on the new hold branch:

hold = time < self.discharge_time and (setpoint < SmartMode.POWER_START or setpoint <= self.discharge_limit)

On master discharge_limit is reset to 0 each cycle and only incremented for devices that were classified into self.discharge. For a single-device installation whose only device is currently charging, self.discharge is empty, so discharge_limit is 0 and hold needs setpoint < 50 W. With a real discharge demand the charging device is released exactly as before. That is a reading of the diff plus master, not something I measured, so please treat it as a question rather than a claim: is the single-device case in scope for the hold, or intentionally out of it?

None of this is an objection to merging. Cutting mode switches from 22 to 3 and removing the cold start is clearly an improvement, and the ±10 W trade-off looks well argued. The point is only that it makes the hold cheaper rather than bounded — a cap on total hold time, or arming once per real transition instead of once per call, would be a separate and complementary change.

@zoic21

zoic21 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed feedback. You’re right that this PR does not bound the total charge delay during repeated discharge/charge transitions; it only makes those transitions less costly. I think limiting that delay would be better handled in a separate PR.

For a single-battery setup, releasing the charging battery when demand exceeds 50 W is intentional, since no other battery can cover the requested discharge.

Also, your measurements were made on the original 1.4.3 behavior. Testing this branch would help quantify the remaining impact.

@qpaz

qpaz commented Jul 27, 2026

Copy link
Copy Markdown

Thanks — that clears up both points. Agreed that bounding the total delay belongs in its own PR, and good to know that releasing the charging battery above 50 W is intentional for a single-battery setup; that answers my question.

Happy to test, with one honest caveat on timing: the effect only shows up when there is real surplus and the battery is well below full, so I need a suitable day rather than a slot I can pick. I cannot promise a date, but I will run it and report with the same metrics as in my comment on #1525 — flip-to-first-charge-command latency, idle plateaus, and the Wh balance over the window — so the numbers are directly comparable to the 1.4.3 baseline. Excitation would be the same thermostat-cycling ~2.2 kW load as in the original report, so it is like-for-like.

Could you rebase the branch before I do? It is currently 3 ahead / 14 behind master and mergeable: false, so it needs a rebase for the merge in any case, and it means I install something that matches my released version rather than a mix.

For what it is worth, I did check whether the staleness would invalidate the comparison, and it would not: between your merge base 29140b9 and 1.4.3, manager.py differs by 6 lines — the SOCFULL solar pass-through and the MATCHING_DISCHARGE change — and the hold-off block itself is byte-identical, just shifted by two lines. So testing the branch as it stands would be defensible too. Your call; I am only asking because you need the rebase anyway.

@zoic21

zoic21 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Hello,
It's rebase, I will wait your return to see if it's fix the issue.
Thanks a lot for your test.

@qpaz

qpaz commented Aug 11, 2026

Copy link
Copy Markdown

@zoic21 — short status so you are not waiting on me.

I am not going to get the staged A/B run done. The excitation I can reproduce is a
thermostat-cycling ~2.1 kW appliance, and a valid run needs it to coincide with at least
45 minutes of PV surplus and with me being free to set things up. Since the rebase on
27 July there have been four such sessions here: in three the house was a net importer
across the whole window (mean grid +1369 W, +3046 W and +836 W, exporting only 26 %, 0 %
and 8 % of the time), so there was no charge power to hold back in the first place. The
fourth was this morning, the only one with real surplus, and it was over after 22 minutes.
I would rather say this plainly than leave the PR parked on a promise. Please do not block
the merge on me — and note that @mggevaer has already run this branch for several days
and posted data.

Two things that may still be useful.

The 1.4.3 behaviour is unchanged and still reproducing here. During those 22 minutes
this morning, on stock 1.4.3: bat_in_out flat at 0 W from 10:09:59 to 10:19:25 — 9.5
minutes with no charge command while the meter was exporting; input_limit repeatedly
ramping 50 → 54 → 58 → ~700 W and being reset to 0; SoC pinned at 10 % throughout.

The part that matters for #1525 is the separate PR you mentioned. You wrote that
bounding the total charge delay would be better handled on its own, and I agree — that
bound, not this PR, is what would fix the case I reported. If you open it, I am happy to
review the diff, and once it is in a release I will report flip-to-first-charge-command
latencies and idle-plateau counts from normal operation, measured the same way as the
25 July 1.4.3 baseline so the numbers stay comparable.

Also worth a look for the underlying issue: in #1525 @uwegoexe-boop independently reported
the same pattern on 9 August with different hardware (SolarFlow 2400 AC+, 3 batteries,
Shelly Pro 3EM, 1.4.3) — charging collapsing to ~0 W while PV production rose, surplus
exported instead of stored, for roughly 2.5 hours, SoC dropping from 24 % to 21 %.

@Mrdauphi

Copy link
Copy Markdown

I don't have much experience with formal testing, so I can't offer a proper A/B test. Just my practical experience running this for a while.

I've been running this on the rebased branch for over a week now, and I can confirm this feature works well for my situation.

I noticed that when running my oven, it doesn't draw power steadily but fluctuates during operation. This caused my batteries (2x 800+) to keep switching between discharging, idle, and charging, especially when there was enough solar surplus to charge from.

This change now keeps the batteries at a steady ~10W discharge most of the time, while still being able to ramp up quickly when the oven draws more power. So for my setup, it makes the charging/discharging behavior noticeably more stable.

@mggevaer

Copy link
Copy Markdown

@Mrdauphi Perhaps I didn't notice an issue because my focus has been more on the excess surplus not being captured. While this fix might perhaps do more for preventing import when the battery is not yet empty.
However at the end of the day I often don't have a fully charged battery yet so any loss of charging is usually a loss of savings on my end, while there's plenty of time to discharge the battery fully during the night.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants