Skip to content

fix(enphase): stop schedule conflicts wedging battery control - #4428

Merged
springfall2008 merged 6 commits into
mainfrom
enphase-schedule-conflicts
Aug 3, 2026
Merged

fix(enphase): stop schedule conflicts wedging battery control#4428
springfall2008 merged 6 commits into
mainfrom
enphase-schedule-conflicts

Conversation

@springfall2008

Copy link
Copy Markdown
Owner

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() took details[0], but the Enphase cloud orders that list by updatedAt. 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 as CONFLICTING_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:

Cause Count
Overlap with a sibling in the same family 241
Overlap with the other family's stale window 31
Unexplained 0

The clearest instance:

  • 22:26:21 — writes DTG 0d1b771422:35-23:30, 200 OK
  • 22:26:25 — GET returns the pair reordered; Predbat adopts 1b5b3878
  • 22:31:22 — writes DTG 1b5b387822:50-23:30409, against its own window from five minutes earlier

By 09:36 both families were failing on every 5-minute cycle, with the battery left running on stale windows.

Changes

  • Pin the adopted schedule. Match the adopted scheduleId on 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.
  • Own the family. 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 once on conflict. Re-read schedules on a 409 and retry, so a stale cached view cannot wedge the cycle. The re-read both refreshes the id and prunes the stray sibling. A conflict surviving the re-read is a real clash and is not retried again.
  • Retire windows with DELETE, not isEnabled: False. The cloud answers 200 to a disable while leaving isEnabled true 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_json gained an allow_empty flag 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:

--- PRE-FIX code ---   FAIL@1722: assert written == [.../first, .../first]
--- POST-FIX code ---  PASS

./run_pre_commit passes — 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_schedule during an earlier run — the createdAt timestamps 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

springfall2008 and others added 2 commits August 3, 2026 12:10
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>
Copilot AI review requested due to automatic review settings August 3, 2026 11:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: false to DELETE, and extends request_json() with allow_empty to 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.

Comment thread docs/components.md Outdated
Comment thread apps/predbat/tests/test_enphase_api.py Outdated
springfall2008 and others added 4 commits August 3, 2026 13:14
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>
@springfall2008
springfall2008 merged commit ef9775e into main Aug 3, 2026
2 checks passed
@springfall2008
springfall2008 deleted the enphase-schedule-conflicts branch August 3, 2026 11:23
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.

2 participants