Skip to content

EnphaseCloud: activate CFG/DTG/RBD modes after schedule write to fix stuck pending schedules#4253

Closed
dan1elhughes wants to merge 10 commits into
springfall2008:mainfrom
dan1elhughes:fix/issue-4252-enphase-cfg-activation
Closed

EnphaseCloud: activate CFG/DTG/RBD modes after schedule write to fix stuck pending schedules#4253
dan1elhughes wants to merge 10 commits into
springfall2008:mainfrom
dan1elhughes:fix/issue-4252-enphase-cfg-activation

Conversation

@dan1elhughes

@dan1elhughes dan1elhughes commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Enphase Cloud writes schedules correctly (HTTP 200, correct times and limits), but schedules remain in "pending" status indefinitely because the Enphase Cloud API requires a follow-up activation call to commit each schedule type to the gateway:

  • CFG (charge-from-grid): PUT /batterySettings/{id} with acceptedItcDisclaimer
  • DTG (discharge-to-grid): PUT /batterySettings/{id} with dtgControl.enabled
  • RBD (restrict-battery-discharge): PUT /batterySettings/{id} with rbdControl.enabled

Without these activation calls, schedules are queued but never committed. The PR adds _activate_cfg_mode, _activate_dtg_mode, and _activate_rbd_mode, called after each successful schedule write when the corresponding mode is enabled.

Each activation method logs on failure and invalidates the cached schedule entry so the next apply cycle re-detects the diff and retries write + activation (avoiding stuck-pending schedules from transient API failures).

Fixes #4252.

…ding schedules

After writing a CFG (charge-from-grid) schedule via the Enphase Cloud API,
a second activation call is required to transition the schedule from
"pending" to "active" status. Without this call, the Enphase gateway never
picks up the schedule and the battery never charges.

This adds _activate_cfg_mode which PUTs /batterySettings/<site_id>
with chargeFromGrid: true and the acceptedItcDisclaimer timestamp,
mirroring the "Apply" button behaviour in the Enphase app. The method is
called after every CFG schedule write (when charge is enabled), using
the existing _write_schedule return value to avoid unnecessary API calls.

The existing _ensure_charge_from_grid only handles the one-time ITC
disclaimer and omits both the acceptedItcDisclaimer timestamp and
?userId=<u>&source=enho query params required by the activation flow.

Fixes springfall2008#4252
@dan1elhughes
dan1elhughes marked this pull request as ready for review July 14, 2026 19:42
@chinedu40

chinedu40 commented Jul 15, 2026

Copy link
Copy Markdown

Hi @springfall2008 - this is what I mentioned on our conversation on facebook. Myself and @dan1elhughes maintain a custom integration for home assistant in which this is required. @dan1elhughes could you also include fixes for dtg and rbd if you have time, otherwise i'll try and work on this at the weekend.

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 addresses an Enphase Cloud behavior where CFG schedules remain stuck in "pending" after a successful schedule write by adding a follow-up activation call that transitions the schedule to "active" so the gateway actually applies it.

Changes:

  • Added EnphaseAPI._activate_cfg_mode() to PUT batterySettings/{site_id} with acceptedItcDisclaimer after a CFG schedule write.
  • Updated apply_battery_schedule() to call _activate_cfg_mode() only when the CFG schedule write actually changed something and charge is enabled.
  • Added unit tests covering activation behavior and gating conditions (charge disabled / no-op schedule write).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
apps/predbat/enphase.py Adds CFG activation call after schedule write and wires it into schedule application flow.
apps/predbat/tests/test_enphase_api.py Adds coverage for the new activation call and its gating logic.

Comment thread apps/predbat/enphase.py Outdated
Comment thread apps/predbat/enphase.py Outdated
Comment thread apps/predbat/tests/test_enphase_api.py Outdated
Review fixes:
- Use timezone-aware datetime in _activate_cfg_mode
- Guard battery_settings cache update behind API success in _set_charge_from_grid
- Replace fragile year-based test assertion with recency check
- Extract shared _set_charge_from_grid helper to reduce duplication

DTG/RBD activation (confirmed from real Enphase web app traffic):
- Add _activate_dtg_mode: PUT {dtgControl: {enabled: true}} after DTG write
- Add _activate_rbd_mode: PUT {rbdControl: {enabled: true}} after RBD write
- Wire activation calls into apply_battery_schedule, gated like CFG
- Add 7 tests covering unit + integration for both families

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment thread apps/predbat/tests/test_enphase_api.py Outdated
…chedules_match is True when cloud CFG matches desired schedule

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

Comment thread apps/predbat/enphase.py Outdated
Comment thread apps/predbat/enphase.py
Comment thread apps/predbat/enphase.py
Comment thread apps/predbat/tests/test_enphase_api.py
- _activate_cfg_mode: return bool and log warning on failure
- _activate_dtg_mode: return bool and log warning on failure
- _activate_rbd_mode: return bool and log warning on failure
- MockEnphaseAPI: record params in request_log
- Tests: assert source/userId params on all activation calls

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment thread apps/predbat/enphase.py
Comment thread apps/predbat/enphase.py
Comment thread apps/predbat/enphase.py
@dan1elhughes

Copy link
Copy Markdown
Contributor Author

Would be lovely if Copilot could provide all it's feedback at once!

…t apply retries

When _write_schedule succeeded but _activate_cfg_mode/_activate_dtg_mode/
_activate_rbd_mode failed, the optimistic cache update left the cached
schedule matching the cloud — making _write_schedule a no-op on the next
cycle and preventing activation retry. Now each activation method calls
_invalidate_cached_schedule on failure, which removes startTime from the
cached entry (keeping the id) so schedules_equal detects a diff and
_write_schedule re-issues a PUT update plus activation next time.

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment thread apps/predbat/enphase.py
…n invalidation

Removing startTime from the cache causes schedules_equal to return
'not enabled', which incorrectly no-ops a disable request after an
activation failure. Setting it to an empty string forces a permanent
mismatch regardless of the desired enabled state, so both re-enabled
and disabled requests are correctly written to the cloud.

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

Comment thread apps/predbat/enphase.py Outdated
Comment thread apps/predbat/enphase.py Outdated
Comment thread apps/predbat/enphase.py
Comment thread apps/predbat/tests/test_enphase_api.py Outdated
…e and fix backward compat

- _set_charge_from_grid now takes explicit params kwarg (default None) so
  _ensure_charge_from_grid can omit params it didn't previously send,
  preserving backward compat with the original direct PUT call.
- chargeFromGrid is forced to True after building the body from extra_body,
  preventing callers from accidentally overriding it.
- Fix _invalidate_cached_schedule docstring to accurately describe when
  schedules_equal returns False (when enabled=True; when enabled=False it
  returns True — which is the desired no-op for disable requests).
- Relax age >= 0 assertion to >= -5 to tolerate minor clock skew.

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment thread apps/predbat/enphase.py
@dan1elhughes dan1elhughes changed the title EnphaseCloud: activate CFG mode after schedule write to fix stuck pending schedules EnphaseCloud: activate CFG/DTG/RBD modes after schedule write to fix stuck pending schedules Jul 16, 2026
@dan1elhughes
dan1elhughes requested a review from Copilot July 16, 2026 18:13

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment thread apps/predbat/enphase.py
Comment thread apps/predbat/enphase.py
Comment thread apps/predbat/enphase.py
… status is pending

If a schedule from a previous run is stuck in pending status on the cloud,
schedules_equal matches the cached entry (it ignores status) and
_write_schedule no-ops. Added _is_schedule_pending check: when the schedule
matches but cloud status reports 'pending', activation is attempted without
re-writing — covering the bootstrap case where a schedule was written but
never activated, and the normal flow where activation fails (cache
invalidation) is handled separately.
@dan1elhughes
dan1elhughes force-pushed the fix/issue-4252-enphase-cfg-activation branch from cc55631 to bdb2c0f Compare July 16, 2026 18:19
@dan1elhughes
dan1elhughes requested a review from Copilot July 16, 2026 18:21

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment thread apps/predbat/enphase.py
Comment thread apps/predbat/enphase.py
Comment thread apps/predbat/enphase.py
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment thread apps/predbat/enphase.py
Comment thread apps/predbat/enphase.py
Comment thread apps/predbat/tests/test_enphase_api.py
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@springfall2008

Copy link
Copy Markdown
Owner

Thanks a lot for this work, @dan1elhughes — the activation approach here is exactly right.

Closing in favour of #4263, which is built directly on top of your commits (your authorship is preserved in that PR's history) and adds a few review fixes on top:

  • a crash fix in _is_schedule_pending when the cached schedule status is None (the shape get_schedules produces when the cloud omits scheduleStatus), which could abort apply_battery_schedule in steady state;
  • clearing the pending marker on DTG/RBD activation success (as CFG already did);
  • de-duplicating the three activation methods/wiring blocks; and
  • removing the redundant cold-start batterySettings PUT.

Full credit to you for the original fix — please take a look at #4263 and let me know if anything looks off. 🙏

springfall2008 added a commit that referenced this pull request Jul 17, 2026
…4253) (#4263)

* EnphaseCloud: activate CFG mode after schedule write to fix stuck pending schedules

After writing a CFG (charge-from-grid) schedule via the Enphase Cloud API,
a second activation call is required to transition the schedule from
"pending" to "active" status. Without this call, the Enphase gateway never
picks up the schedule and the battery never charges.

This adds _activate_cfg_mode which PUTs /batterySettings/<site_id>
with chargeFromGrid: true and the acceptedItcDisclaimer timestamp,
mirroring the "Apply" button behaviour in the Enphase app. The method is
called after every CFG schedule write (when charge is enabled), using
the existing _write_schedule return value to avoid unnecessary API calls.

The existing _ensure_charge_from_grid only handles the one-time ITC
disclaimer and omits both the acceptedItcDisclaimer timestamp and
?userId=<u>&source=enho query params required by the activation flow.

Fixes #4252

* EnphaseCloud: address CFG review feedback and add DTG/RBD activation

Review fixes:
- Use timezone-aware datetime in _activate_cfg_mode
- Guard battery_settings cache update behind API success in _set_charge_from_grid
- Replace fragile year-based test assertion with recency check
- Extract shared _set_charge_from_grid helper to reduce duplication

DTG/RBD activation (confirmed from real Enphase web app traffic):
- Add _activate_dtg_mode: PUT {dtgControl: {enabled: true}} after DTG write
- Add _activate_rbd_mode: PUT {rbdControl: {enabled: true}} after RBD write
- Wire activation calls into apply_battery_schedule, gated like CFG
- Add 7 tests covering unit + integration for both families

* Fix incorrect comment in test_apply_no_activate_cfg_when_unchanged: schedules_match is True when cloud CFG matches desired schedule

* EnphaseCloud: log activation failures and validate query params in tests

- _activate_cfg_mode: return bool and log warning on failure
- _activate_dtg_mode: return bool and log warning on failure
- _activate_rbd_mode: return bool and log warning on failure
- MockEnphaseAPI: record params in request_log
- Tests: assert source/userId params on all activation calls

* EnphaseCloud: invalidate cached schedule on activation failure so next apply retries

When _write_schedule succeeded but _activate_cfg_mode/_activate_dtg_mode/
_activate_rbd_mode failed, the optimistic cache update left the cached
schedule matching the cloud — making _write_schedule a no-op on the next
cycle and preventing activation retry. Now each activation method calls
_invalidate_cached_schedule on failure, which removes startTime from the
cached entry (keeping the id) so schedules_equal detects a diff and
_write_schedule re-issues a PUT update plus activation next time.

* EnphaseCloud: set startTime to empty string instead of removing key on invalidation

Removing startTime from the cache causes schedules_equal to return
'not enabled', which incorrectly no-ops a disable request after an
activation failure. Setting it to an empty string forces a permanent
mismatch regardless of the desired enabled state, so both re-enabled
and disabled requests are correctly written to the cloud.

* EnphaseCloud: harden _set_charge_from_grid against extra_body override and fix backward compat

- _set_charge_from_grid now takes explicit params kwarg (default None) so
  _ensure_charge_from_grid can omit params it didn't previously send,
  preserving backward compat with the original direct PUT call.
- chargeFromGrid is forced to True after building the body from extra_body,
  preventing callers from accidentally overriding it.
- Fix _invalidate_cached_schedule docstring to accurately describe when
  schedules_equal returns False (when enabled=True; when enabled=False it
  returns True — which is the desired no-op for disable requests).
- Relax age >= 0 assertion to >= -5 to tolerate minor clock skew.

* EnphaseCloud: activate pending schedules without rewriting when cloud status is pending

If a schedule from a previous run is stuck in pending status on the cloud,
schedules_equal matches the cached entry (it ignores status) and
_write_schedule no-ops. Added _is_schedule_pending check: when the schedule
matches but cloud status reports 'pending', activation is attempted without
re-writing — covering the bootstrap case where a schedule was written but
never activated, and the normal flow where activation fails (cache
invalidation) is handled separately.

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix(enphase): harden schedule activation (null status, DTG/RBD pending, dedupe)

Follow-up review fixes on top of the CFG/DTG/RBD activation work:

- Fix AttributeError in _is_schedule_pending when a matching cached schedule
  has status=None (the shape get_schedules produces when the cloud omits
  scheduleStatus) — this aborted apply_battery_schedule in steady state.
- Clear the cached pending marker on DTG/RBD activation success (as CFG
  already did) so they are not re-activated every apply cycle.
- Collapse the three near-identical activation methods into a shared
  _activate_control_mode helper and the three wiring blocks into
  _write_and_activate; remove the now-dead _set_charge_from_grid.
- Drop the redundant cold-start batterySettings PUT: _ensure_charge_from_grid
  now only accepts the ITC disclaimer and the single activation PUT enables
  chargeFromGrid (force-activated when the setting is still off).

Adds 7 tests covering the null-status path, DTG/RBD pending clearing, the
single-PUT cold start, and the matching-but-not-activated case.

Co-Authored-By: Dan Hughes <github@danhughes.dev>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Dan Hughes <github@danhughes.dev>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

EnphaseCloud: _write_schedule leaves schedules in "pending" — never activated

4 participants