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
- 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 thee-auth-tokenJWT. Sending only the JWT — or just one or two cookies — returns 401. The token script now emits the entire cookie jar as a singlecookieattribute, and everyrest_commandreplays it in theCookieheader. If you were getting 401s, re-copy the updatedget_enphase_token.shand therest_commandblocks below. - Automatic session refresh – the homeowner JWT can stay valid for days while the session cookies expire sooner. On each run
get_enphase_token.shnow probes the battery API (theisValidcall 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'sscan_intervalto900(15 min) so this self-heal runs often enough to keep thecookieattribute live. Timeout for commandfix – HA'scommand_lineintegration kills any command running longer thancommand_timeout(default 15 s). The login/refresh path makes severalcurlcalls, so a slow Enphase response could exceed that. Everycurlnow uses--connect-timeout 8 --max-time 20(fails fast instead of hanging) and thecommand_linesensors setcommand_timeout: 60for headroom.
- Update script to automatically fetch battery ID and User ID
October 2025
- 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.
- Home Assistant (core or supervised)
- Your Enphase Enlighten login
- Basic knowledge of YAML and bash
- Installed packages:
curl,jq(for token 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.shsensor:
- 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_idAfter 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
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}' 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' }}"
} 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 }}
}
} 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 }}
}
}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: trueCalling
script.enphase_refresh_sessionfrom another script blocks until it finishes, so the command below only runs once a fresh session is confirmed.
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: singletoggle_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: singleaction: rest_command.enphase_battery_restrict_discharge
data:
battery_id: "YOUR_BATTERY_ID"
user_id: "YOUR_USER_ID"
restrict: "true"
response_variable: enphaseChange restrict to false to turn it off.
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: truePaste 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
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
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.
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 0Make it executable:
chmod +x /config/get_enphase_schedules_json.sh
⸻
- 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
- otherAfter 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.
⸻
- 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.
⸻
- 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.
- Avoid hard-coding tokens — use
sensor.enphase_jwtdynamically - 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