Skip to content

Latest commit

 

History

60 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

⚡ Enphase IQ Battery Integration for Home Assistant (Updated June 2026)

This guide provides a fully automated setup to control Charge from Grid and Discharge to Grid for Enphase IQ Batteries via the Enlighten API.

It includes:

  • 🪪 Automated JWT and XSRF token retrieval every 15 minutes
  • 🔄 Home Assistant configuration for charge/discharge toggles
  • 🧪 Required validation before toggling

NOTES - June 2026

  1. 401 Unauthorized fix – Enphase's battery API now authenticates off the full Enlighten session cookie jar (the Rails session cookie, enlighten_manager_token_production, BP-XSRF-Token, …), not just the e-auth-token JWT. Sending only the JWT — or just one or two cookies — returns 401. The token script now emits the entire cookie jar as a single cookie attribute, and every rest_command replays it in the Cookie header. If you were getting 401s, re-copy the updated get_enphase_token.sh and the rest_command blocks below.
  2. Automatic session refresh – the homeowner JWT can stay valid for days while the session cookies expire sooner. On each run get_enphase_token.sh now probes the battery API (the isValid call it already makes for the XSRF token); if the session has expired (401/403) it automatically does a fresh login to mint a new cookie jar. Set the JWT sensor's scan_interval to 900 (15 min) so this self-heal runs often enough to keep the cookie attribute live.
  3. Timeout for command fix – HA's command_line integration kills any command running longer than command_timeout (default 15 s). The login/refresh path makes several curl calls, so a slow Enphase response could exceed that. Every curl now uses --connect-timeout 8 --max-time 20 (fails fast instead of hanging) and the command_line sensors set command_timeout: 60 for headroom.

NOTES - January 2026

  1. Update script to automatically fetch battery ID and User ID

October 2025

  1. When creating an automation or script in home assistant to turn off cfg, dtg, rbd, making sure you toggle first, then 2 or 3 seconds later, delete the schedule if needed.

📌 Prerequisites

  • Home Assistant (core or supervised)
  • Your Enphase Enlighten login
  • Basic knowledge of YAML and bash
  • Installed packages: curl, jq (for token script)

🔍 Step 1 – – Automate Enphase JWT Token Retrieval

1.1 Create a Shell Script

Save this script as /config/get_enphase_token.sh and make sure you fill in your details at the top of the script:

set -euo pipefail

EMAIL="xxxxxx"     # enlighten email
PASSWORD="xxxxxxx" # enlighten password

# Leave these empty to auto-discover
BATTERY_ID="${BATTERY_ID:-}"
USER_ID="${USER_ID:-}"

WORKDIR="/config"
COOKIES="$WORKDIR/cookies.txt"
HDRS="$WORKDIR/headers.txt"
JWT_FILE="$WORKDIR/jwt.txt"

# Fail fast instead of letting a stalled request hang past HA's command_timeout.
CURL_OPTS=(--connect-timeout 8 --max-time 20)

# ------------------ helpers ------------------

b64url_decode() {
  # base64url -> raw bytes (best-effort)
  local s="${1:-}"
  s="${s//_/\/}"
  s="${s//-/+}"
  local pad=$(( (4 - ${#s} % 4) % 4 ))
  s="${s}$(printf '=%.0s' $(seq 1 "$pad"))"
  printf '%s' "$s" | base64 -d 2>/dev/null || true
}

jwt_payload_json() {
  local jwt="${1:-}"
  local payload
  payload="$(printf '%s' "$jwt" | cut -d. -f2)"
  b64url_decode "$payload"
}

jwt_exp() {
  local jwt="${1:-}"
  local payload
  payload="$(jwt_payload_json "$jwt")"
  printf '%s' "$payload" | jq -r '.exp // 0' 2>/dev/null || echo 0
}

cookies_present() {
  [[ -s "$COOKIES" ]]
}

# Auto-discover numeric site/battery id + numeric user id (your proven HAR method)
discover_ids() {
  local final_url site_id user_id

  # Site/battery id from final post-login URL (supports /web/<id>/..., /pv/systems/<id>/..., /systems/<id>/...)
  final_url="$(
    curl -sS "${CURL_OPTS[@]}" --compressed -L -b "$COOKIES" -c "$COOKIES" \
      -o /dev/null -w "%{url_effective}" \
      "https://enlighten.enphaseenergy.com/"
  )"

  site_id="$(
    printf '%s' "$final_url" \
      | grep -oE "/(web|pv/systems|systems)/[0-9]+" \
      | head -n1 \
      | grep -oE '[0-9]+$' \
      || true
  )"

  if [[ -z "${site_id:-}" || ! "$site_id" =~ ^[0-9]+$ ]]; then
    echo "ERROR: could not extract site/battery id from final URL: $final_url" >&2
    return 1
  fi

  # Numeric userId from app-api/<site>/data.json
  user_id="$(
    curl -sS "${CURL_OPTS[@]}" --compressed -b "$COOKIES" -c "$COOKIES" \
      "https://enlighten.enphaseenergy.com/app-api/${site_id}/data.json?app=1&device_status=non_retired&is_mobile=0" \
      | jq -r '.app.userId // .app.user_id // .app.user.id // empty' 2>/dev/null \
      || true
  )"

  if [[ -z "${user_id:-}" || ! "$user_id" =~ ^[0-9]+$ ]]; then
    echo "ERROR: could not extract numeric userId from app-api/${site_id}/data.json" >&2
    return 1
  fi

  # Populate globals only if not already set
  [[ -n "${BATTERY_ID:-}" ]] || BATTERY_ID="$site_id"
  [[ -n "${USER_ID:-}"   ]] || USER_ID="$user_id"

  return 0
}

# ------------------ functions ------------------

get_jwt_and_login() {
  : > "$COOKIES"
  : > "$HDRS"

  # Fetch authenticity token
  local auth_token
  auth_token="$(
    curl -sS "${CURL_OPTS[@]}" --compressed -c "$COOKIES" 'https://enlighten.enphaseenergy.com/login' \
      | sed -n 's/.*name="authenticity_token" value="\([^"]*\)".*/\1/p'
  )"

  [[ -n "${auth_token:-}" ]] || { echo "ERROR: authenticity_token not found" >&2; return 1; }

  # Login (creates session cookies)
  curl -sS "${CURL_OPTS[@]}" --compressed -b "$COOKIES" -c "$COOKIES" \
    -X POST 'https://enlighten.enphaseenergy.com/login/login' \
    -H 'Content-Type: application/x-www-form-urlencoded' \
    --data "utf8=%E2%9C%93&authenticity_token=${auth_token}&user[email]=${EMAIL}&user[password]=${PASSWORD}" \
    >/dev/null

  # Get JWT
  local jwt_json jwt_token
  jwt_json="$(
    curl -sS "${CURL_OPTS[@]}" --compressed -b "$COOKIES" -c "$COOKIES" \
      'https://enlighten.enphaseenergy.com/app-api/jwt_token.json'
  )"
  jwt_token="$(printf '%s' "$jwt_json" | jq -r '.token // empty')"

  [[ -n "${jwt_token:-}" ]] || { echo "ERROR: JWT token not returned (login failed?)" >&2; return 1; }

  printf '%s' "$jwt_token" > "$JWT_FILE"

  # Discover numeric IDs using the now-authenticated session
  discover_ids
}

jwt_valid() {
  [[ -s "$JWT_FILE" ]] || return 1
  local jwt exp now
  jwt="$(<"$JWT_FILE")"
  exp="$(jwt_exp "$jwt")"
  now="$(date +%s)"
  # valid if >1h left
  [[ "$exp" -gt $((now + 3600)) ]]
}

# Probe the battery API and return the HTTP status. Also refreshes BP-XSRF-Token in
# the cookie jar / response headers. Used both to obtain the XSRF token and to detect
# an expired session (401/403) so we can re-login.
battery_isvalid() {
  local jwt
  jwt="$(<"$JWT_FILE")"

  curl -sS "${CURL_OPTS[@]}" --compressed -D "$HDRS" -b "$COOKIES" -c "$COOKIES" \
    -o /dev/null -w '%{http_code}' \
    "https://enlighten.enphaseenergy.com/service/batteryConfig/api/v1/battery/sites/${BATTERY_ID}/schedules/isValid" \
    -H 'content-type: application/json' \
    -H 'origin: https://battery-profile-ui.enphaseenergy.com' \
    -H 'referer: https://battery-profile-ui.enphaseenergy.com/' \
    -H "e-auth-token: ${jwt}" \
    -H "username: ${USER_ID}" \
    --data-raw '{"scheduleType":"dtg"}' 2>/dev/null || echo "000"
}

extract_xsrf() {
  local xsrf_token
  xsrf_token="$(awk '$6 == "BP-XSRF-Token" { print $7 }' "$COOKIES" | tail -n1 || true)"
  if [[ -z "${xsrf_token:-}" ]]; then
    xsrf_token="$(grep -i 'Set-Cookie: *BP-XSRF-Token=' "$HDRS" \
      | sed -E 's/.*BP-XSRF-Token=([^;]+).*/\1/' | tail -n1 || true)"
  fi

  printf '%s' "$xsrf_token"
}

# ------------------ main ------------------

# Ensure we have a valid JWT AND a logged-in cookie jar that lets us discover ids / call battery endpoints
# If any of these are missing, do a fresh login.
need_login=0
jwt_valid || need_login=1
cookies_present || need_login=1
[[ -n "${BATTERY_ID:-}" && -n "${USER_ID:-}" ]] || need_login=1

if [[ "$need_login" -eq 1 ]]; then
  get_jwt_and_login
else
  # Even with valid JWT, auto-fill ids if user left them blank
  [[ -n "${BATTERY_ID:-}" && -n "${USER_ID:-}" ]] || discover_ids
fi

# Hard fail if still missing (you confirmed these must be numeric)
[[ "${BATTERY_ID:-}" =~ ^[0-9]+$ ]] || { echo "ERROR: BATTERY_ID not set / not numeric" >&2; exit 1; }
[[ "${USER_ID:-}"   =~ ^[0-9]+$ ]] || { echo "ERROR: USER_ID not set / not numeric" >&2; exit 1; }

jwt="$(<"$JWT_FILE")"

# The homeowner JWT can stay valid for days while the Enlighten session cookies expire
# sooner; when that happens the probe returns 401/403, so force a fresh login to refresh
# the WHOLE cookie jar (Rails session + manager token + XSRF). Healthy runs add no extra
# requests - this isValid call is also how we fetch the XSRF token.
http_code="$(battery_isvalid)"
if [[ "$http_code" == "401" || "$http_code" == "403" ]]; then
  echo "Session probe returned ${http_code}; refreshing session via full login" >&2
  get_jwt_and_login || true
  http_code="$(battery_isvalid)"
fi

xsrf="$(extract_xsrf)"

# The battery API authenticates off the FULL Enlighten session cookie jar, not just
# the JWT. Sending only e-auth-token (or a couple of cookies) returns 401. Emit every
# cookie from the jar (Rails session, enlighten_manager_token_production, BP-XSRF-Token,
# ...) as one Cookie header string for Home Assistant to replay on each request.
cookie_header="$(awk -F'\t' '
  NF>=7 && $5 ~ /^[0-9]+$/ && $1 ~ /enphaseenergy\.com/ {
    if (out) out = out "; "
    out = out $6 "=" $7
  }
  END { print out }
' "$COOKIES")"

exp="$(jwt_exp "$jwt")"

status="OK"
if [[ -z "${jwt:-}" || -z "${xsrf:-}" || -z "${cookie_header:-}" \
      || ! "$http_code" =~ ^2[0-9][0-9]$ ]]; then
  status="PARTIAL"
fi

echo "{\"status\":\"${status}\",\"token\":\"${jwt}\",\"xsrf\":\"${xsrf}\",\"cookie\":\"${cookie_header}\",\"exp\":${exp},\"user_id\":${USER_ID},\"battery_id\":${BATTERY_ID}}"

Make it executable:

chmod +x /config/get_enphase_token.sh

1.2 Add the Token Sensor to configuration.yaml

sensor:
  - platform: command_line
    name: "Enphase JWT"
    command: "bash /config/get_enphase_token.sh"
    command_timeout: 60  # allow time for the full login + session-refresh path
    scan_interval: 900  # every 15 minutes
    value_template: "{{ value_json.status }}"
    json_attributes:
      - token
      - xsrf
      - cookie
      - exp
      - battery_id
      - user_id

1.3 Access the JWT in Home Assistant

After restarting Home Assistant:

  • Go to Developer Tools → States
  • Look for sensor.enphase_jwt
  • Use {{ state_attr('sensor.enphase_jwt', 'token') }} and {{ state_attr('sensor.enphase_jwt', 'xsrf') }} to reference the token in service calls

🧪 Step 2 – Validation Rest Commands (Required!)

These commands must run before toggling battery settings or the PUT requests will silently fail.

rest_command:
  enphase_validate_dtg:
    url: "https://enlighten.enphaseenergy.com/service/batteryConfig/api/v1/battery/sites/{{ battery_id }}/schedules/isValid"
    method: post
    headers:
      content-type: "application/json"
      e-auth-token: "{{ state_attr('sensor.enphase_jwt', 'token') }}"
      x-xsrf-token: "{{ state_attr('sensor.enphase_jwt', 'xsrf') }}"
      username: "{{ user_id }}"
      origin: "https://battery-profile-ui.enphaseenergy.com"
      referer: "https://battery-profile-ui.enphaseenergy.com/"
      cookie: "{{ state_attr('sensor.enphase_jwt', 'cookie') }}"
    payload: '{"scheduleType":"dtg"}'

  enphase_validate_cfg:
    url: "https://enlighten.enphaseenergy.com/service/batteryConfig/api/v1/battery/sites/{{ battery_id }}/schedules/isValid"
    method: post
    headers:
      content-type: "application/json"
      e-auth-token: "{{ state_attr('sensor.enphase_jwt', 'token') }}"
      x-xsrf-token: "{{ state_attr('sensor.enphase_jwt', 'xsrf') }}"
      username: "{{ user_id }}"
      origin: "https://battery-profile-ui.enphaseenergy.com"
      referer: "https://battery-profile-ui.enphaseenergy.com/"
      cookie: "{{ state_attr('sensor.enphase_jwt', 'cookie') }}"
    payload: '{"scheduleType":"cfg","forceScheduleOpted":true}'

🔁 Step 3 – Rest Commands to Toggle Charging/Discharging

3.1 Charge from Grid

  enphase_battery_charge_from_grid:
    url: "https://enlighten.enphaseenergy.com/service/batteryConfig/api/v1/batterySettings/{{ battery_id }}?userId={{ user_id }}&source=enho"
    method: put
    headers:
      content-type: "application/json"
      e-auth-token: "{{ state_attr('sensor.enphase_jwt', 'token') }}"
      x-xsrf-token: "{{ state_attr('sensor.enphase_jwt', 'xsrf') }}"
      username: "{{ user_id }}"
      origin: "https://battery-profile-ui.enphaseenergy.com"
      referer: "https://battery-profile-ui.enphaseenergy.com/"
      cookie: "{{ state_attr('sensor.enphase_jwt', 'cookie') }}"
    payload: >
      {
        "chargeFromGrid": {{ charge }},
        "acceptedItcDisclaimer": "{{ now().strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] ~ 'Z' }}"
      }

3.2 Discharge to Grid

  enphase_battery_discharge_to_grid:
    url: "https://enlighten.enphaseenergy.com/service/batteryConfig/api/v1/batterySettings/{{ battery_id }}?userId={{ user_id }}&source=enho"
    method: put
    headers:
      content-type: "application/json"
      e-auth-token: "{{ state_attr('sensor.enphase_jwt', 'token') }}"
      x-xsrf-token: "{{ state_attr('sensor.enphase_jwt', 'xsrf') }}"
      username: "{{ user_id }}"
      origin: "https://battery-profile-ui.enphaseenergy.com"
      referer: "https://battery-profile-ui.enphaseenergy.com/"
      cookie: "{{ state_attr('sensor.enphase_jwt', 'cookie') }}"
    payload: >
      {
        "dtgControl": {
          "enabled": {{ discharge }}
        }
      }

3.3 Restrict Battery Discharge

  enphase_battery_restrict_discharge:
    url: "https://enlighten.enphaseenergy.com/service/batteryConfig/api/v1/batterySettings/{{ battery_id }}?userId={{ user_id }}&source=enho"
    method: put
    headers:
      content-type: "application/json"
      e-auth-token: "{{ state_attr('sensor.enphase_jwt', 'token') }}"
      x-xsrf-token: "{{ state_attr('sensor.enphase_jwt', 'xsrf') }}"
      username: "{{ user_id }}"
      origin: "https://battery-profile-ui.enphaseenergy.com"
      referer: "https://battery-profile-ui.enphaseenergy.com/"
      cookie: "{{ state_attr('sensor.enphase_jwt', 'cookie') }}"
    payload: >
      {
        "rbdControl": {
          "enabled": {{ restrict }}
        }
      }

▶️ Step 4 – Scripts to Toggle Charging and Discharging

4.0 Refresh-session helper (avoids a one-off 401)

The token sensor self-heals every 15 minutes (Step 1), but a command fired in the gap right after a session dies could still 401 once. This reusable script forces the sensor to re-run (which probes the battery API and re-logs in if needed) and waits for a fresh, healthy result. Call script.enphase_refresh_session as the first step of any battery command to close that gap.

enphase_refresh_session:
  alias: Enphase – Refresh Session
  description: >-
    Force the Enphase token sensor to re-run so it probes the battery API and, if the
    session has expired, logs in again to mint a fresh cookie jar. Call this before a
    battery rest_command so a session that died between the sensor's scans can't cause
    a one-off 401.
  mode: single
  sequence:
    - variables:
        t0: "{{ now().timestamp() }}"
    - action: homeassistant.update_entity
      target:
        entity_id: sensor.enphase_jwt
    # The cookie attribute changes every run, so last_updated advances once the refresh
    # completes. Wait for a NEW, healthy result before returning.
    - wait_template: >-
        {{ states.sensor.enphase_jwt.last_updated.timestamp() > t0
           and states('sensor.enphase_jwt') == 'OK' }}
      timeout: "00:00:25"
      continue_on_timeout: true

Calling script.enphase_refresh_session from another script blocks until it finishes, so the command below only runs once a fresh session is confirmed.

4.1 Toggle Charge from Grid

toggle_enphase_charge_from_grid:
  alias: Toggle Enphase Charge from Grid
  description: Enable or disable Charge from Grid mode
  fields:
    charge:
      description: true to enable, false to disable
      example: true
  sequence:
    - service: script.enphase_refresh_session
    - service: rest_command.enphase_validate_cfg
    - delay: "00:00:01"
    - service: rest_command.enphase_battery_charge_from_grid
      data:
        charge: "{{ charge }}"
  mode: single

4.2 Toggle Discharge to Grid

toggle_enphase_discharge_to_grid:
  alias: Toggle Enphase Discharge to Grid
  description: Enable or disable Discharge to Grid mode
  fields:
    discharge:
      description: true to enable, false to disable
      example: true
  sequence:
    - service: script.enphase_refresh_session
    - service: rest_command.enphase_validate_dtg
    - delay: "00:00:01"
    - service: rest_command.enphase_battery_discharge_to_grid
      data:
        discharge: "{{ discharge }}"
  mode: single

4.3 Restrict Battery Discharge - On

action: rest_command.enphase_battery_restrict_discharge
data:
  battery_id: "YOUR_BATTERY_ID"
  user_id: "YOUR_USER_ID"
  restrict: "true"
response_variable: enphase

Change restrict to false to turn it off.


✅ Example Automation

automation:
  - alias: Enable Charge from Grid at 02:00
    trigger:
      - platform: time
        at: "02:00:00"
    action:
      - service: script.toggle_enphase_charge_from_grid
        data:
          charge: true

▶️ Step 5 – Scheduling


🧩 Setup Instructions

1. Add to configuration.yaml

Paste the following under your rest_command: section:

rest_command:
  enphase_add_schedule:
    url: "https://enlighten.enphaseenergy.com/service/batteryConfig/api/v1/battery/sites/{{ battery_id }}/schedules"
    method: post
    headers:
      content-type: "application/json"
      e-auth-token: "{{ state_attr('sensor.enphase_jwt', 'token') }}"
      x-xsrf-token: "{{ state_attr('sensor.enphase_jwt', 'xsrf') }}"
      username: "{{ user_id }}"
      origin: "https://battery-profile-ui.enphaseenergy.com"
      referer: "https://battery-profile-ui.enphaseenergy.com/"
      cookie: "{{ state_attr('sensor.enphase_jwt', 'cookie') }}"
    payload: >
      {
        "timezone": "Europe/London",
        "startTime": "{{ start_time[:5] }}",
        "endTime": "{{ end_time[:5] }}",
        "limit": {{ limit }},
        "scheduleType": "{{ schedule_type }}",
        "days": [ {% for d in days %}{{ d | int }}{% if not loop.last %}, {% endif %}{% endfor %} ]
      }

Then restart Home Assistant or reload the YAML config. NOTE: setting the limit is important depending on if adding a schedule for charging or discharging as it will stop at the limit set. When it reaches the limit it will run from grid/Solar

2. Add Script in the UI (Scripts Editor)

Go to Settings → Automations & Scenes → Scripts → + Add Script Then paste the following:

alias: Add Enphase Battery Schedule
sequence:
  - service: rest_command.enphase_add_schedule
    data:
      start_time: "{{ start_time }}"
      end_time: "{{ end_time }}"
      schedule_type: "{{ schedule_type }}"
      days: "{{ days }}"
      user_id: "{{ user_id }}"
      battery_id: "{{ battery_id }}"
      limit: {{ limit }}
fields:
  start_time:
    description: "Start time (e.g. '02:00')"
    example: "02:00"
  end_time:
    description: "End time (e.g. '03:00')"
    example: "03:00"
  schedule_type:
    description: "Type of schedule: CFG (charge), DTG (discharge to grid), RBD (reserve battery discharge)"
    selector:
      select:
        options:
          - CFG
          - DTG
          - RBD
  days:
    description: "Select days to apply schedule (Mon=1 to Sun=7)"
    selector:
      select:
        multiple: true
        mode: list
        options:
          - label: Monday
            value: "1"
          - label: Tuesday
            value: "2"
          - label: Wednesday
            value: "3"
          - label: Thursday
            value: "4"
          - label: Friday
            value: "5"
          - label: Saturday
            value: "6"
          - label: Sunday
            value: "7"
  user_id:
    description: "User ID (default: 1234567)"
    default: 1234567
  battery_id:
    description: "Battery ID (default: 1234567)"
    default: 1234567
  limit:
    description: " Charge/Discharge limit"
    selector:
      number:
        min: 6
        max: 100
        mode: box
        step: 1
    default: 100 #set 100 if charging or 6 if discharging 
mode: single
icon: mdi:battery-clock

6 🗑️ Enphase Battery — Delete Schedules from Home Assistant (REST Method)

This guide lets you list and delete Enphase schedules (CFG / DTG / RBD) inside Home Assistant using a command_line sensor and a REST command that mirrors the browser request.

Works nicely with Predbat: clear out overlapping schedules and re-apply your desired state.


1) Create the “get schedules” script

File: /config/get_enphase_schedules_json.sh

#!/usr/bin/env bash
# get_enphase_schedules_json.sh
# Fetch Enphase schedule IDs (CFG, DTG, RBD) and output as JSON for Home Assistant.
#
# Expects these environment variables (set by the "Enphase Schedules" command_line sensor):
#   ENPHASE_AUTH    - JWT             ({{ state_attr('sensor.enphase_jwt','token') }})
#   ENPHASE_XSRF    - XSRF token      ({{ state_attr('sensor.enphase_jwt','xsrf') }})
#   ENPHASE_COOKIE  - full cookie jar ({{ state_attr('sensor.enphase_jwt','cookie') }})
#
# The battery API authenticates off the full Enlighten session cookie jar; without
# it the /schedules call returns 401.

set -uo pipefail  # tolerate curl/jq failures but catch unset vars

SITE_ID="YOUR_SITE_ID"
USERNAME="YOUR_USER_ID"
LOG_FILE="/config/enphase_debug.log"

{
  echo
  echo "========== $(date '+%F %T') =========="
  echo "Script started"
  echo "AUTH length: ${#ENPHASE_AUTH}, XSRF: ${ENPHASE_XSRF:-missing}, COOKIE: $([ -n "${ENPHASE_COOKIE:-}" ] && echo present || echo missing)"
} >> "$LOG_FILE"

# --- Validate tokens ---
if [[ -z "${ENPHASE_AUTH:-}" || -z "${ENPHASE_XSRF:-}" || -z "${ENPHASE_COOKIE:-}" ]]; then
  echo '{"error":"Missing or empty tokens"}'
  echo "Missing or empty tokens" >> "$LOG_FILE"
  exit 0
fi

BASE_URL="https://enlighten.enphaseenergy.com/service/batteryConfig/api/v1/battery/sites/${SITE_ID}"

COMMON_HEADERS=(
  -H "accept: application/json, text/plain, */*"
  -H "content-type: application/json"
  -H "origin: https://battery-profile-ui.enphaseenergy.com"
  -H "referer: https://battery-profile-ui.enphaseenergy.com/"
  -H "username: ${USERNAME}"
  -H "x-xsrf-token: ${ENPHASE_XSRF}"
  -H "e-auth-token: ${ENPHASE_AUTH}"
  -H "cookie: ${ENPHASE_COOKIE}"
)

# --- Fetch data ---
# Fail fast instead of letting a stalled request hang past HA's command_timeout.
JSON=$(curl -sS --connect-timeout 8 --max-time 20 "${BASE_URL}/schedules" "${COMMON_HEADERS[@]}" 2>>"$LOG_FILE" || echo "")
echo "Raw response length: ${#JSON}" >> "$LOG_FILE"

if [[ -z "$JSON" ]]; then
  echo '{"error":"Empty response from API"}'
  echo "Empty API response" >> "$LOG_FILE"
  exit 0
fi

# --- Validate JSON ---
if ! echo "$JSON" | jq empty >/dev/null 2>&1; then
  SHORT=$(echo "$JSON" | head -c 200 | sed 's/"/\\"/g')
  echo "{\"error\":\"Invalid or non-JSON response\",\"preview\":\"${SHORT}...\"}"
  echo "Invalid JSON: ${SHORT}" >> "$LOG_FILE"
  exit 0
fi

# --- Extract schedule IDs properly ---
OUTPUT=$(echo "$JSON" | jq -c '
{
  cfg: (
    (.cfg.details // []) |
    map({
      id: .scheduleId,
      start: .startTime,
      end: .endTime,
      limit: .limit,
      days: .days,
      enabled: .isEnabled
    })
  ),
  dtg: (
    (.dtg.details // []) |
    map({
      id: .scheduleId,
      start: .startTime,
      end: .endTime,
      limit: .limit,
      days: .days,
      enabled: .isEnabled
    })
  ),
  rbd: (
    (.rbd.details // []) |
    map({
      id: .scheduleId,
      start: .startTime,
      end: .endTime,
      limit: .limit,
      days: .days,
      enabled: .isEnabled
    })
  ),
  other: []
}
')

echo "$OUTPUT"
echo "Output: $OUTPUT" >> "$LOG_FILE"

exit 0

Make it executable:

chmod +x /config/get_enphase_schedules_json.sh

  1. Create the command_line sensor

configuration.yaml (or split file):

command_line:
  - sensor:
      name: "Enphase Schedules"
      command: >
        /bin/bash -c 'ENPHASE_AUTH="{{ state_attr("sensor.enphase_jwt", "token") }}"
        ENPHASE_XSRF="{{ state_attr("sensor.enphase_jwt", "xsrf") }}"
        ENPHASE_COOKIE="{{ state_attr("sensor.enphase_jwt", "cookie") }}"
        /config/get_enphase_schedules_json.sh'
      command_timeout: 60
      scan_interval: 30
      value_template: "OK"
      json_attributes:
        - cfg
        - dtg
        - rbd
        - other

After reloading, check Developer Tools → States → sensor.enphase_schedules. You should see arrays of schedule objects (id, start, end, limit, days, enabled) under cfg, dtg, rbd.

  1. Create the REST command (delete by ID)

configuration.yaml:

rest_command:
  enphase_delete_schedule:
    url: >-
      https://enlighten.enphaseenergy.com/service/batteryConfig/api/v1/battery/sites/{{ battery_id }}/schedules/{{ schedule_id }}/delete
    method: POST
    headers:
      Accept: "application/json, text/plain, */*"
      Content-Type: "application/json"
      Origin: "https://battery-profile-ui.enphaseenergy.com"
      Referer: "https://battery-profile-ui.enphaseenergy.com/"
      Username: "{{ user_id }}"
      X-XSRF-Token: "{{ state_attr('sensor.enphase_jwt', 'xsrf') }}"
      Cookie: "{{ state_attr('sensor.enphase_jwt', 'cookie') }}"
      E-Auth-Token: "{{ state_attr('sensor.enphase_jwt', 'token') }}"
      User-Agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0 Safari/537.36"
      Connection: "close"
      TE: "trailers"
    payload: "{}"
    content_type: "application/json"

Test it in Developer Tools → Services:

service: rest_command.enphase_delete_schedule
data:
  schedule_id: XXXX-XXXX-XXX-XXX

Expected response: {"message":"success"} If you see 403, wait for sensor.enphase_jwt to refresh or add a short delay before calling.

  1. Create the user-selectable script (cfg / dtg / rbd / all)

scripts.yaml:

  alias: "Delete Enphase Schedules by Type"
  mode: single
  fields:
    schedule_type:
      name: "Schedule Type"
      description: "Select which schedule(s) to delete"
      required: true
      selector:
        select:
          options:
            - cfg
            - dtg
            - rbd
            - all
    battery_id:
      name: "Battery ID"
      description: "Your Enphase Site/Battery ID"
      required: true
    user_id:
      name: "User ID"
      description: "Your Enphase User ID"
      required: true
  sequence:
    - variables:
        schedules:
          cfg: "{{ state_attr('sensor.enphase_schedules', 'cfg') or [] }}"
          dtg: "{{ state_attr('sensor.enphase_schedules', 'dtg') or [] }}"
          rbd: "{{ state_attr('sensor.enphase_schedules', 'rbd') or [] }}"
        types_to_delete: >
          {% if schedule_type == 'all' %}
            ['cfg','dtg','rbd']
          {% else %}
            [schedule_type]
          {% endif %}
    - repeat:
        for_each: "{{ types_to_delete }}"
        sequence:
          - repeat:
              for_each: "{{ schedules[repeat.item] }}"
              sequence:
                - service: rest_command.enphase_delete_schedule
                  data:
                    battery_id: "{{ battery_id }}"
                    user_id: "{{ user_id }}"
                    schedule_id: "{{ repeat.item }}"
          - delay: "00:00:02"
    - service: homeassistant.update_entity
      target:
        entity_id: sensor.enphase_schedules

Usage examples • Call the script from the Services UI with schedule_type: dtg • Or add a Dashboard button that invokes the script with a chosen type.


🧠 Tips & Troubleshooting

  • Avoid hard-coding tokens — use sensor.enphase_jwt dynamically
  • Always validate before PUT requests
  • Tokens expire — ensure your script runs at least every 15 minutes
  • Use Developer Tools → Services in HA to test your scripts

About

Enphase IQ Battery Integration for Home Assistant using REST API

Resources

Stars

12 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors