fix(enphase): stop schedule conflicts wedging battery control - #4428
Merged
Conversation
A site with more than one schedule per family (CFG/DTG/RBD) drove Predbat into a permanent HTTP 409 loop. get_schedules() took details[0], but the cloud orders that list by updatedAt, so writing to the adopted schedule pushed it behind its sibling and the next re-read swapped Predbat onto the sibling. Predbat then wrote a window overlapping the one it had just set itself, which the cloud rejected as CONFLICTING_SCHEDULE_*. In a captured 13-hour log every one of 272 rejections was an overlap with a schedule Predbat was no longer tracking - 241 against the same family, 31 against the other family's stale window - and both families ended up failing on every 5 minute cycle. - Match the adopted scheduleId on re-read instead of taking the first entry, falling back to the first non-deleted one when nothing is adopted or the adopted schedule was removed outside Predbat. - Delete any other schedule in a family when in write mode: Predbat drives one window per direction, and a schedule it does not track cannot be updated or cleared but still blocks overlapping writes. Read-only mode deletes nothing. - Retry a conflicting write once after re-reading, so a stale cached view cannot wedge the cycle; a conflict surviving the re-read is a real clash and is not retried again. - Retire an unwanted window with a DELETE rather than isEnabled=False. The cloud answers 200 to a disable while leaving isEnabled true and still enforcing the window, so disabled windows lingered and conflicted with the other family's next write. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR hardens Predbat’s Enphase integration to prevent schedule “family” conflicts (HTTP 409) that can permanently wedge battery control when the Enphase cloud returns multiple schedules per family and reorders them after updates.
Changes:
- Pins the adopted Enphase schedule ID per family across re-reads (order-independent), and prunes duplicate sibling schedules in write mode.
- Retries a single schedule PUT after a 409 by re-reading schedules (refreshing IDs and removing stray siblings) before retrying once.
- Switches “disable window” behavior from
isEnabled: falseto DELETE, and extendsrequest_json()withallow_emptyto treat 204/no-body deletes as success; adds targeted Enphase API tests and bumps version.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| docs/components.md | Documents the new “Predbat owns Enphase schedules” behavior and DELETE-based window retirement. |
| apps/predbat/tests/test_enphase_api.py | Adds test helpers plus new regression/edge-case tests for adopted ID pinning, sibling pruning, DELETE disable, and 409 retry behavior. |
| apps/predbat/predbat.py | Version bump to v8.47.4. |
| apps/predbat/enphase.py | Implements adopted schedule pinning, sibling pruning in write mode, single retry on 409, DELETE-based retirement, and allow_empty support for 204 responses. |
Validated at n=100 seed 0 against the pre-committed criterion: mean metric -2.0431 (target -2.05), zero regressions, 4.25x runtime (target 4.9x). State isolation confirmed empirically - members inside the portfolio produce bit-identical plans to the same config run standalone. The single divergence from the oracle is the common-horizon rescoring correcting an invalid cross-horizon comparison the oracle made. Default unchanged; two known defects recorded for follow-up. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This reverts commit 710183c.
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.
Problem
A site with more than one schedule per family (CFG/DTG/RBD) drives Predbat into a permanent HTTP 409 loop, after which it can no longer control the battery.
get_schedules()tookdetails[0], but the Enphase cloud orders that list byupdatedAt. Writing to the adopted schedule pushes it behind its sibling, so the next re-read swaps Predbat onto the sibling. Predbat then writes a window overlapping the one it set itself minutes earlier, and the cloud rejects it asCONFLICTING_SCHEDULE_*.From a 13-hour debug log on an affected site, all 272 rejections were an overlap with a schedule Predbat was no longer tracking:
The clearest instance:
22:26:21— writes DTG0d1b7714→ 22:35-23:30, 200 OK22:26:25— GET returns the pair reordered; Predbat adopts1b5b387822:31:22— writes DTG1b5b3878→ 22:50-23:30 → 409, against its own window from five minutes earlierBy 09:36 both families were failing on every 5-minute cycle, with the battery left running on stale windows.
Changes
scheduleIdon re-read rather than taking the first entry, falling back to the first non-deleted one when nothing is adopted yet or the adopted schedule was removed outside Predbat.isEnabled: False. The cloud answers 200 to a disable while leavingisEnabledtrue and still enforcing the window — confirmed by a GET 29 minutes after a disable showing it still live. Without this, Predbat could never clear its own window and the cross-family conflicts would persist.request_jsongained anallow_emptyflag so a 204/no-body DELETE counts as success.Behaviour change worth flagging
In write mode Predbat now deletes user-created Enphase schedules it finds on the site, including ones made in the Enlighten app. This is intentional — an untracked schedule wedges battery control — and is documented in
docs/components.md, with read-only mode as the escape hatch for anyone who wants to manage schedules themselves. It is not recoverable from Predbat's side, so it may deserve a release-note callout.Testing
Nine new tests in
test_enphase_api.py, written test-first. Five failed for the expected reason before the fix; three guard the edge cases the new logic must not break (fallback adoption, read-only). The ninth replays the exact production sequence above and was verified against stashed pre-fix code:./run_pre_commitpasses — all 10 hooks green, full quick suite green, interrogate at 100%.Version bumped v8.47.3 → v8.47.4.
Follow-up not addressed here
I could not tell from the log whether the duplicate schedules originated in the user's Enlighten app or from Predbat's own POST path in
_write_scheduleduring an earlier run — thecreatedAttimestamps predate the capture. If it is the latter, there is a second bug still to find: something creating duplicates rather than updating in place.🤖 Generated with Claude Code