Skip to content

EnphaseCloud: activate CFG/DTG/RBD after schedule write (supersedes #4253)#4263

Merged
springfall2008 merged 10 commits into
mainfrom
fix/issue-4252-enphase-activation
Jul 17, 2026
Merged

EnphaseCloud: activate CFG/DTG/RBD after schedule write (supersedes #4253)#4263
springfall2008 merged 10 commits into
mainfrom
fix/issue-4252-enphase-activation

Conversation

@springfall2008

Copy link
Copy Markdown
Owner

Supersedes #4253 (by @dan1elhughes) and fixes #4252. The original commits are preserved with their authorship — this branch is built directly on top of @dan1elhughes's work and layers on review fixes for correctness and cleanup.

What the base work does (credit: @dan1elhughes)

Enphase Cloud schedule writes land in "pending" status and never commit to the gateway until a follow-up PUT /batterySettings/{id} activates each mode:

  • CFG (charge-from-grid): acceptedItcDisclaimer (+ chargeFromGrid)
  • DTG (discharge-to-grid): dtgControl.enabled
  • RBD (restrict-battery-discharge): rbdControl.enabled

Review fixes added on top

  1. Crash fix (blocking): _is_schedule_pending did entry.get("status", "").lower(), which raised AttributeError when the cached status was None — the shape get_schedules produces when the cloud omits scheduleStatus. This aborted apply_battery_schedule in the common steady state (matching, enabled schedule), and because CFG runs first it also skipped the DTG/RBD writes+activations. Now null-safe.
  2. DTG/RBD pending marker: cleared on activation success (as CFG already did) so a successfully-activated schedule isn't re-activated on every subsequent apply cycle.
  3. De-duplication: the three near-identical activation methods collapse into one _activate_control_mode helper, and the three near-identical wiring blocks into _write_and_activate; the now-dead _set_charge_from_grid is removed.
  4. Redundant cold-start PUT removed: _ensure_charge_from_grid now only accepts the one-time ITC disclaimer; the single activation PUT enables chargeFromGrid (force-activated when the setting is still off), which also closes the matching-but-never-activated gap.

Testing

7 new/updated unit tests: null-status path (no crash), DTG/RBD pending-marker clearing, single-PUT cold start, and matching-but-not-activated activation. Full ./run_all --quick suite passes; pre-commit clean.

Note: unit tests mock the HTTP layer, so they confirm the correct activation requests are sent under the right conditions. Confirming the real gateway transitions pending → active still needs a live Enphase account.

🤖 Generated with Claude Code

dan1elhughes and others added 10 commits July 14, 2026 20:40
…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 #4252
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
…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.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…g, 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>

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 updates the EnphaseCloud integration to ensure written schedules actually become effective on the gateway by issuing the required post-write activation PUT /batterySettings/{id} for CFG/DTG/RBD modes, and adds/updates unit tests to validate the activation behavior and related edge cases.

Changes:

  • Add a shared activation helper and wire schedule writes through a _write_and_activate flow that activates when a schedule is written, stuck pending, or needs a control-setting enable.
  • Make pending-status detection null-safe and improve retry behavior by invalidating cached schedules on activation failure and clearing pending markers on activation success.
  • Extend Enphase API tests to cover activation calls (including query params), pending/no-crash cases, and “matches-but-not-activated” scenarios.

Reviewed changes

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

File Description
apps/predbat/enphase.py Adds activation + pending-handling helpers and updates schedule application to activate CFG/DTG/RBD after writes (or when pending/needs enable).
apps/predbat/tests/test_enphase_api.py Updates HTTP mock logging to capture query params and adds unit tests covering activation, pending behavior, cache invalidation, and null-status handling.

@springfall2008
springfall2008 merged commit 8fa3e56 into main Jul 17, 2026
3 checks passed
@springfall2008
springfall2008 deleted the fix/issue-4252-enphase-activation branch July 17, 2026 08:18
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

3 participants