Last active
February 4, 2025 18:22
-
-
Save koleson/d78da48497575455b9be6a3c96951bd1 to your computer and use it in GitHub Desktop.
Script to set SunPower SunVault ESS discharge mode via SunPower GraphQL API
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env zsh | |
# Created 5 April 2023 by Kiel Oleson - [email protected] - @kielo | |
# updated 6 July 2024 | |
# | |
# example usage: ./sunpower_ess_mode_set.sh -m SELF_CONSUMPTION -r 0.04 -s A_123456 -u [email protected] -p SuperSecret1! | |
# | |
# README/NOTES: https://gist.github.com/koleson/db9df38ef6051715d743e572acebdd4d | |
# | |
# default reserve level to provide if none is specified at command line | |
reserve_level=0.07 | |
# provided as-is without warranty :) | |
help_message="not implemented lol - source code is your best bet" | |
while (( $# )); do | |
case $1 in | |
-h|--help) printf "${help_message}" && return ;; | |
-u|--username) shift; username=$1 ;; | |
-p|--password) shift; password=$1 ;; | |
-m|--mode) shift; new_mode=$1 ;; | |
-r|--reserve) shift; provided_reserve_level=$1 ;; | |
-s|--site) shift; site_id=$1 ;; | |
-v|--verbose) flag_verbose=true ;; | |
-*) opterr $1 && return 2 ;; | |
*) positional+=("${@[@]}"); break ;; | |
esac | |
shift | |
done | |
case $username in | |
"") | |
echo "Username (-u/--username) is required." | |
exit 1 | |
;; | |
esac | |
case $site_id in | |
A_*) | |
# echo "Site ID ${site_id} looks good to me" | |
;; | |
"") | |
echo "Site ID (-s/--site) required." | |
exit 1 | |
;; | |
*) | |
echo "Site ID ${site_id} looks sus - usually they start with \"A_\", but I'll roll with it." | |
;; | |
esac | |
# from sunpower dl_cgi swagger - valid modes, my assumed descriptions: | |
# "UNKNOWN", | |
# "STANDBY", Probably the pre-Permission To Operate state | |
# "MANUAL_CHARGE", Unclear - if you know what this does, let me know! | |
# "MANUAL_DCM", DCM = "Demand Charge Management?" | |
# "DCM", DCM = "Demand Charge Management?" | |
# "TARIFF_OPTIMIZER", Unclear - if you know what this does, let me know! | |
# "ENERGY_ARBITRAGE", ("Cost Savings" in SunPower app) | |
# "SELF_CONSUMPTION", ("Self-Supply" in SunPower app) | |
# "BACKUP_ONLY", Only discharge when off-grid. | |
# "HECO_ZERO_EXPORT" Hawaiian Electric Company gives no credit for exports 9AM-4PM. | |
case $new_mode in | |
STANDBY|MANUAL_CHARGE|MANUAL_DCM|DCM|TARIFF_OPTIMIZER|ENERGY_ARBITRAGE|SELF_CONSUMPTION|BACKUP_ONLY|HECO_ZERO_EXPORT) | |
echo "changing ESS to mode ${new_mode}..." | |
;; | |
*) | |
echo "invalid target ESS state ${new_mode} - see script source for valid modes"; | |
exit 1;; | |
esac | |
case $provided_reserve_level in | |
[01]|0.*) | |
# echo "valid reserve level provided" | |
reserve_level="${provided_reserve_level}" | |
;; | |
[-][0-9]*([.][0-9]*) | [0-9]*) | |
echo "valid number but not a valid reserve level - needs to be between 0 and 1, inclusive" | |
exit 1 | |
;; | |
"") | |
echo "valid reserve level not provided but i guess that's ok? - using default of ${reserve_level}" | |
;; | |
*) | |
echo "reserve value provided (${provided_reserve_level}) but not valid - needs to be a float between 0 and 1, inclusive" | |
exit 1 | |
;; | |
esac | |
shift $((OPTIND-1)) | |
# we can actually generate the login and graphql mutations with just the provided info - better for debugging to do so before attempting the http requests. | |
login_data=$(jq --null-input --arg username "${username}" --arg password "${password}" '{"remember": "true", "username": $username, "password": $password}') | |
# echo "login data: ${login_data}" | |
params_json=$(jq --null-input --arg siteid "${site_id}" --arg mode "${new_mode}" --argjson reservelevel $reserve_level '{"siteKey": $siteid, "mode": $mode, "reserveLevel": $reservelevel}') | |
mutation='"mutation ChangeBatteryMode($params: NewBatteryModeInput!) {\n changeBatteryMode(params: $params) {\n siteKey\n label\n value\n }\n}\n"' | |
change_query='{ "query": '"${mutation}"', "variables": { "params": '"${params_json}"'}}' | |
# echo "params json: ${params_json}" | |
# echo "query: ${change_query}" | |
# when debugging json/query generation, it's often useful to bail out early | |
# TODO: add dry-run flag to control this. kmo 5 apr 2023 18h39 | |
# exit 20 | |
# in theory, we could reuse the access token / renew it if needed / only log in if needed | |
# TODO: persist and reuse auth data | |
echo "logging in..." | |
login_result=$( { | |
curl -s 'https://edp-api.edp.sunpower.com/v1/auth/okta/signin' \ | |
-X POST \ | |
-H 'Host: edp-api.edp.sunpower.com' \ | |
-H 'User-Agent: sunpower-graphql (1.0) CFNetwork/1406.0.4 Darwin/22.4.0' \ | |
-H 'Connection: keep-alive' \ | |
-H 'Accept: */*' \ | |
-H 'Accept-Language: en-US,en;q=0.9' \ | |
-H "Content-Length: ${#login_data}" \ | |
-H 'Authorization: Bearer undefined' \ | |
-H 'Content-Type: application/json; charset=utf-8' \ | |
-d "${login_data}" | |
} ) | |
# echo "login result: ${login_result}" | |
access_token=$( echo "${login_result}" | jq -r .access_token ) | |
case $access_token in | |
"") | |
echo "access token doesn't look very valid... exiting" | |
echo "response to auth attempt: ${login_result}" | |
exit 1 | |
;; | |
esac | |
echo "logged in." | |
# now, actually update the mode | |
echo "updating ESS mode..." | |
update_result=$( { curl -s 'https://edp-api-graphql.edp.sunpower.com/graphql' \ | |
-H 'Accept-Encoding: gzip, deflate, br' \ | |
-H 'Content-Type: application/json' \ | |
-H 'Accept: application/json' \ | |
-H 'Connection: keep-alive' \ | |
-H 'Origin: altair://-' \ | |
-H "Authorization: Bearer ${access_token}" \ | |
--data-binary "${change_query}" \ | |
--compressed } ) | |
# confirm updates by echoing relevant part of API response | |
if jq -e . >/dev/null 2>&1 <<< "${update_result}" ; then | |
# echo "Parsed JSON successfully and got something other than false/null" | |
# echo "${update_result}" | |
resulting_mode=$(echo "${update_result}" | jq .data.changeBatteryMode ) | |
echo "success - resulting mode info: ${resulting_mode}" | |
else | |
echo "failed to parse response JSON" | |
fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment