Skip to content

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

Description

@dan1elhughes

Describe the bug

EnphaseCloud inverter: Predbat writes the CFG schedule to the Enphase Cloud API (HTTP 200, correct times/limits), but the schedule stays in "scheduleStatus": "pending" indefinitely. The Enphase gateway never picks it up, so the battery never charges from the grid.

The schedule write itself is correct — the API returns the expected values. But the schedule never transitions from "pending" to "active", and the battery never charges. This happens on every write cycle, including after container restarts (clean cache).

The Enphase Cloud API requires a second call to activate a written schedule: a PUT /batterySettings/{id}?userId={u}&source=enho with {"chargeFromGrid": true, "acceptedItcDisclaimer": "<current-iso8601-timestamp>"}. This is the equivalent of pressing "Apply" in the Enphase app. Without it, the schedule is queued but never committed to the gateway.

Expected behaviour

After Predbat writes a CFG schedule, the Enphase gateway should pick it up and start charging the battery from the grid during the charge window. The schedule should transition to "scheduleStatus": "active" and the battery should show positive charge power.

Predbat version

v8.45.2

Environment details

  • Inverter: EnphaseCloud (IQ 5P, 3× Encharge batteries, ~15 kWh usable capacity)
  • Install: Docker container (standalone, not HA add-on), Debian bookworm-slim
  • Mode: Control charge & discharge (set_read_only: False)
  • Enphase config: enphase_automatic: False — our PV is on a separate dumb string inverter measured by myenergi, not Enphase microinverters. We don't want the Enphase Cloud auto-managing the battery against its own (empty) production readings, which would conflict with Predbat's plan and use incorrect generation data.
  • Tariff: Octopus Intelligent Go via BottlecapDave HA integration

Screenshots

N/A (API-level bug — schedule status is the evidence)

Log file

Every _write_schedule call returns "scheduleStatus": "pending":

2026-07-14 19:32:13.011176: Enphase: Updating CFG schedule d9873c9e-... on site 5886607: 19:30-23:59 limit=100 enabled=True
2026-07-14 19:32:13.389959: Enphase API: PUT .../battery/sites/5886607/schedules/d9873c9e-... -> 200
  {"scheduleStatus": "pending", "startTime": "19:30", "endTime": "23:59", "limit": 100, "isEnabled": true}

2026-07-14 20:05:57.591157: Enphase: Updating CFG schedule d9873c9e-... on site 5886607: 19:00-23:59 limit=100 enabled=True
2026-07-14 20:05:57.989006: Enphase API: PUT .../battery/sites/5886607/schedules/d9873c9e-... -> 200
  {"scheduleStatus": "pending", "startTime": "19:00", "endTime": "23:59", "limit": 100, "isEnabled": true}

2026-07-14 20:23:57.077385: Enphase API: PUT .../battery/sites/5886607/schedules/d9873c9e-... -> 200
  {"scheduleStatus": "pending", "startTime": "19:35", "endTime": "23:59", "limit": 100, "isEnabled": true}

The periodic GET /schedules/ also confirms the schedule is stuck in pending:

2026-07-14 19:58:31.362534: Enphase API: GET .../battery/sites/5886607/schedules -> 200
  {"cfg": {"scheduleStatus": "pending", ...}, "dtg": {"scheduleStatus": "pending", ...},
   "rbd": {"scheduleStatus": "active", ...}, "anySchedulePending": true}

After manually calling PUT /batterySettings/{id}?userId={u}&source=enho with {"chargeFromGrid": true, "acceptedItcDisclaimer": "2026-07-14T20:25:00"}, the schedule immediately transitions to "active" and the battery starts charging at ~9.4 kW.

Root cause analysis

apply_battery_schedule only calls _write_schedule (the PUT /schedules/ endpoint). It never makes the PUT /batterySettings/ activation call.

_ensure_charge_from_grid does call PUT /batterySettings/ but is not equivalent:

  • Calls a separate POST /batterySettings/acceptDisclaimer/ instead of including acceptedItcDisclaimer in the same payload
  • Omits ?userId={u}&source=enho query params
  • Is cached (runs once), so subsequent schedule changes aren't activated
  • Runs before _write_schedule, not after

Evidence table

Action Schedule status Result
Predbat _write_schedule only (5+ cycles) "pending" Battery does not charge (0 W)
Container restart + fresh write (clean cache) "pending" Battery does not charge
PUT /batterySettings/ with acceptedItcDisclaimer timestamp "active" Battery charges at 9.4 kW
Same schedule PUT (no-op) + activation call "active" Battery charges

The activation call alone — without changing the schedule — moves "pending" → "active", confirming the schedule write is correct and only the activation is missing.

Proposed fix

Add a method that mirrors the activation call, and invoke it after every CFG schedule write:

async def _activate_cfg_mode(self, site_id):
    """Activate charge-from-grid mode after writing the CFG schedule."""
    params = {"source": "enho"}
    if self.user_id:
        params["userId"] = self.user_id
    now_iso = datetime.now().isoformat()
    await self.request_json(
        "PUT",
        f"{BATTERY_CONFIG_BASE}/batterySettings/{site_id}",
        family="battery_config",
        params=params,
        json_body={
            "chargeFromGrid": True,
            "acceptedItcDisclaimer": now_iso,
        },
    )
    self.battery_settings.setdefault(site_id, {})["chargeFromGrid"] = True

Then in apply_battery_schedule (~line 821-825):

  if charge.get("enable"):
      await self._ensure_charge_from_grid(site_id)
- wrote |= await self._write_schedule(site_id, SCHEDULE_CHARGE, ...)
+ wrote_cfg = await self._write_schedule(site_id, SCHEDULE_CHARGE, ...)
+ if wrote_cfg and charge.get("enable"):
+     await self._activate_cfg_mode(site_id)
+ wrote |= wrote_cfg

_ensure_charge_from_grid stays for the one-time ITC disclaimer. The new call fires on every schedule change.

Metadata

Metadata

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions