Repository navigation
fix(deye,sunsynk): share the TOU builder, pad correctly and split at midnight - #5159
Merged
Merged
Conversation
DEYE's 6-slot TOU programme is padded out with fillers at fixed clock hours, and each filler carried the idle baseline whatever time it landed on. DEYE reads a slot as running until the next slot's start, so a filler inside a window silently ended it there while Predbat's plan still showed it running. Fillers now derive their state at their own time, so one inside a window carries that window's action and one outside it still derives the baseline. Fixes #5156 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…midnight The 6-slot TOU programme was padded out at fixed clock hours (04:00, 08:00, 12:00) carrying the idle baseline. Every slot is always in force, so padding that landed inside a window silently ended it there while Predbat's plan still showed it running - any window crossing one of those hours, including an overnight charge crossing 04:00. Padding now follows the last real slot at 5-minute steps carrying that slot's own state, so it is inert wherever it lands. It falls back to the widest gap when the day has no room left. Separately, no slot may span midnight, and a wrapping window was never split - its post-midnight half was dropped because the 00:00 slot was always seeded idle. A wrapping window is now written as a pre-midnight and a post-midnight slot, so it charges unbroken across midnight. Both defects existed identically in deye.py and sunsynk.py, which carried copies of the same builder. The programme logic now lives in TouScheduleMixin and each component keeps only its own slot encoding. Fixes #5156 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
springfall2008
marked this pull request as ready for review
September 19, 2026 14:03
Contributor
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
Overlapping charge and export windows can produce an incorrect idle interval in the generated schedule.
Review effort: Lite
Findings: None
What changed in this PR
Refactors DEYE/Sunsynk TOU scheduling into a shared builder, fixing padding boundaries and midnight-spanning windows.
Changes:
- Adds shared TOU construction with state-preserving padding and midnight splitting.
- Updates both integrations to use vendor-specific encoders.
- Adds regression tests and removes obsolete filler constants.
- The shared builder still needs correction for overlapping charge and export windows.
| File | Summary |
|---|---|
apps/predbat/unit_test.py |
Registers the shared TOU test suite. |
apps/predbat/tou_schedule.py |
Implements shared TOU construction; overlapping windows have an unresolved correctness issue. |
apps/predbat/tests/test_tou_schedule.py |
Adds shared builder regression coverage. |
apps/predbat/tests/test_sunsynk_const.py |
Updates tests for removed filler constants. |
apps/predbat/tests/test_deye_control.py |
Adds DEYE regression coverage. |
apps/predbat/sunsynk.py |
Uses the shared builder with Sunsynk encoding. |
apps/predbat/sunsynk_const.py |
Removes obsolete filler constants. |
apps/predbat/deye.py |
Uses the shared builder with DEYE encoding. |
apps/predbat/deye_const.py |
Removes obsolete filler constants. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The mocked tests can say what build_tou_slots produces; nothing said the inverter stores it. --tou-test writes a padded programme to a real inverter, reads it back to check every slot time and flag landed, then restores the original. Safety: the original System Mode group is written to a JSON file before anything is sent, the restore runs from a finally and is itself verified by reading back, and the test window is placed hours from the current time so the inverter never acts on the programme written here. --apps-yaml reads credentials from a real apps.yaml, which is also the only way this CLI can drive an oauth-token account - --username/--password cannot express one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ters Found while confirming the GH#5156 fix on real hardware. The settings object exposes the TOU register block a second time under unrelated names, so keys that read like independent settings are the same memory at a different scale. Useful twice over: it independently proves a TOU write reached the real registers rather than a cloud-side echo, and it makes SUNSYNK_SYSTEM_MODE_FIELDS excluding those keys load-bearing rather than incidental. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
springfall2008
deleted the
fix/deye-tou-filler-truncates-window-5156
branch
September 19, 2026 14:40
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is an automated draft PR generated from issue #5156 — a maintainer should review it before merging.
Fixes #5156
Summary
Two defects in the 6-slot Time-of-Use programme, both present identically in
deye.pyandsunsynk.py.Padding placed at fixed clock hours (the reported bug). The programme was padded out from a fixed list — 04:00, 08:00, 12:00 — with each padding slot carrying the idle baseline. Every slot is always in force, so a padding slot landing inside a window is a boundary the inverter obeys: it silently ended the window there while Predbat's plan still showed it running. Any window strictly crossing one of those hours was affected, including the commonest shape of all, an overnight charge crossing 04:00.
Padding now follows the last real slot at 5-minute steps and carries that slot's own state, so it is inert wherever it lands — the state does not change at its boundary. When the last slot is too close to midnight to fit (a window ending 23:50), it falls back to the widest gap, which is always wide enough: with at most 5 slots the widest gap is at least 1440/5 minutes against the 25 minutes five padding slots need.
Wrapping windows were never split. No slot may span midnight, and the
00:00slot was seeded idle unconditionally, so a 22:00–02:00 charge emitted boundaries at 22:00 and 02:00 and simply dropped its post-midnight half. It is now written as two slots — 22:00–00:00 and 00:00–02:00 — and charges unbroken across midnight. Note the failure mode was not the inverter rejecting or resetting the programme: slot 1 at 00:00 always terminated slot 6, so the charge was just silently lost.Sharing. These were two copies of the same builder, which is why the same bug existed twice. The programme logic now lives in
TouScheduleMixin(tou_schedule.py) and each component keeps only its own slot encoding — DEYE a JSON list, Sunsynk positionalsellTime1..6fields. Net −321 lines across the two components.TOU_FILLER_TIMESis gone from both const modules; the CONFIRMED Sunsynk timer documentation that lived beside it moved into the shared module, where it now documents the rules the builder implements.The one behavioural difference between the two copies was resolved in favour of Sunsynk's:
_window_activenormalises via_to_slot_timebefore comparing, which is equivalent on valid input and safer on malformed input.Testing
coverage/run_pre_commitpasses — all hooks, full quick suite. All 15 DEYE and Sunsynk test modules pass.New
test_tou_schedule.py(registered inTEST_REGISTRY) tests the shared builder directly against a minimal fake component, so it holds whatever either vendor does with the slots: programme shape across 7 schedule shapes, minute-by-minute window continuity, the midnight split, padding placement and its fallback, the idle 24-hour case, and zero-length window rejection. The vendor-level tests intest_deye_control.pycover the same properties through the DEYE encoding.Fails without the fix, passes with it. A pure refactor cannot be proved by stashing (the imports break), so the red run reverts the behaviour inside the mixin — fixed-hour padding, no midnight split — while keeping the structure:
The day-2 line reproduces the reporter's captured
timeUseSettingItemspayload exactly.The resulting programmes:
CONFIRMED ON HARDWARE (Sunsynk 2211093089, 2026-09-19) with the new
sunsynk.py --tou-test, which writes a padded programme, reads it back and restores the original:The padding slots at 22:35/22:40/22:45 carry self-use — the state they follow — so the window runs unbroken to 22:30 and the programme is unchanged by their presence. The inverter that ran this was itself sitting on
00:00/04:00/08:00/12:00/16:00/20:00, the factory default and exactly the fixed-hour list the old padding drew from.The DEYE side remains mock-verified only; it is the same builder behind a different cloud.
impact()before editing:build_tou_slotsand_active_stateare both LOW on each side, contained within their own component, no execution flows affected.detect_changes({scope: "compare", base_ref: "main"})after: low, 6 changed symbols, 0 affected, no affected processes.Notes
test_idle_schedule_holds_one_state_all_day.voltN/currentNin the Sunsynk settings object alias the TOU registers. The live round trip revealed thatvolt1-volt4aresellTime3-sellTime6as minutes/50,volt5-volt10aresellTime1Pac-sellTime6Pac/10, andvolt11,volt12,current1-current4aresellTime1Volt-sellTime6Voltx10 — one contiguous 16-value window, matching 16/16 in both the before and after reads. That is independent proof the write reached the real registers rather than a cloud-side echo, and it meansSUNSYNK_SYSTEM_MODE_FIELDSexcluding those keys is load-bearing: anything that wrote them would corrupt the charge programme. Recorded intools/debug-journal.md; no code change needed, since Predbat already never sends them.