EnphaseCloud: activate CFG/DTG/RBD modes after schedule write to fix stuck pending schedules#4253
Conversation
…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
|
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. |
There was a problem hiding this comment.
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 PUTbatterySettings/{site_id}withacceptedItcDisclaimerafter 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. |
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
…chedules_match is True when cloud CFG matches desired schedule
- _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
|
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.
…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.
…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.
… 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.
cc55631 to
bdb2c0f
Compare
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>
|
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:
Full credit to you for the original fix — please take a look at #4263 and let me know if anything looks off. 🙏 |
…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>
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:
PUT /batterySettings/{id}withacceptedItcDisclaimerPUT /batterySettings/{id}withdtgControl.enabledPUT /batterySettings/{id}withrbdControl.enabledWithout 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.