This guide walks you through how I use Home Assistant, FoxESS APIs, Python and GloBird to maximise my energy benefits. Home Assistant is an open source home automation platform that runs locally and lets you integrate basically anything with a network connection, including your inverter.
My goal was to cap grid export during the GloBird Super Export window (6pm to 9pm) so I export exactly 15 kWh at the premium rate, instead of blowing past the cap and earning almost nothing for the excess.
You’ll end up with:
GloBird Energy is a smaller Aussie retailer that offers a plan called ZEROHERO specifically designed for battery owners. The key features:
ZEROCHARGE Window (11am to 2pm): Grid electricity is completely free. You charge your battery from the grid for $0. This guarantees your battery is full every day regardless of solar conditions.
Super Export (6pm to 9pm): You get paid $0.15/kWh for the first 15 kWh you export back to the grid during the evening peak. After 15 kWh it drops to $0.02/kWh, which is why we cap the export.
ZEROHERO Bonus ($1/day): If you keep your grid imports under 0.09 kWh during the 6pm to 9pm window, you get an extra $1 credit. Basically, as long as your battery is covering the house during peak and you’re not pulling from the grid, you earn the bonus.
Daily supply charge: $1.32/day regardless of usage.
The maths on a good day:
Super Export: 15 kWh × $0.15 = $2.25
ZEROHERO Bonus: = $1.00
Supply charge: = -$1.32
Grid imports: = $0.00 (battery covers everything)
--------
Net: = +$1.93 profit (they pay you)
On a typical week you’ll see some days where cloud cover or high usage cuts into this, but the free midday charging means your battery is always full by 2pm regardless of weather. The worst case scenario is a day where your house draws more than the battery can supply during peak, you miss the ZEROHERO bonus, and you still earn the Super Export. You’re almost never paying for electricity.
Without controlling the export limit, your inverter will dump as much power to the grid as it can during ForceDischarge. With a 10kW inverter, that’s potentially 10 kWh per hour, meaning you’d hit 15 kWh in 90 minutes and spend the remaining 90 minutes earning the low rate. You’d also drain your battery way faster than necessary and risk needing grid power overnight.
The fix is simple maths: 5 kW x 3 hours = 15 kWh. Cap the export at 5000W and you perfectly fill the Super Export allowance without overshooting. If your battery is smaller or your house uses more power during that window, bump it to 5500W or 6000W to compensate for house load eating into your export.
Home Assistant solves this by:
The FoxESS app has no concept of “pace my export to a target kWh over a time window.” It just does whatever rate you tell it, as fast as possible. That’s where HA fills the gap.
Here’s a real example of the daily cycle:
| Time | Battery SoC | What’s Happening |
|---|---|---|
| 6:00 AM | 52% | Overnight drain done, solar starting to trickle in |
| 8:00 AM | 48% | Morning loads (heating, kettle, etc.) pulling from battery |
| 11:00 AM | 45% | ZEROCHARGE window opens, ForceCharge kicks in |
| 1:55 PM | 100% | Battery full from free grid charging |
| 5:55 PM | 100% | HA sets export limit to 5000W |
| 6:00 PM | 99% | ForceDischarge starts, exporting at ~5kW |
| 7:30 PM | 78% | Midway through window, on track |
| 9:00 PM | 58% | Window ends, ~15 kWh exported |
| 9:05 PM | 58% | HA resets export limit to 14500W |
| 11:00 PM | 52% | Evening loads drew from battery, now coasting overnight |
Revenue for this day: $2.25 (super export) + $1.00 (ZEROHERO) = $3.25 earned, minus $1.32 supply = +$1.93 net profit.
The battery starts the next morning around 45 to 52%, which is plenty. It never drops below the minimum SoC (10%) and has enough headroom to survive until the next ZEROCHARGE window at 11am.
foxesscloud Python library installed inside your HA containerYou’ll need both of these for the scripts.
The scripts use the foxesscloud pip package. Install it inside your Home Assistant container:
docker exec homeassistant pip install foxesscloud requests
After every HA update (when the container image gets replaced), you’ll need to run this again. Worth adding to your startup routine or a post-update script.
These go in your HA config scripts directory. If your HA config is mounted at /opt/docker/homeassistant/config, create a scripts folder there.
mkdir -p /opt/docker/homeassistant/config/scripts
This reads the current export limit from your inverter. It outputs just the number (in watts) so HA can use it as a sensor value.
#!/usr/bin/env python3
"""Read FoxESS current export limit via Cloud API.
Outputs the numeric value (watts) to stdout.
"""
import sys
import io
# Suppress any stdout output from the foxesscloud library (it prints a banner on import)
_real_stdout = sys.stdout
sys.stdout = io.StringIO()
try:
import foxesscloud.openapi as f
finally:
sys.stdout = _real_stdout
API_KEY = "YOUR_API_KEY_HERE"
DEVICE_SN = "YOUR_DEVICE_SERIAL_HERE"
def main():
old_stdout = sys.stdout
sys.stdout = io.StringIO()
try:
f.api_key = API_KEY
f.device_sn = DEVICE_SN
f.get_device()
result = f.get_named_settings("ExportLimit")
finally:
sys.stdout = old_stdout
if isinstance(result, dict):
value = result.get("value", 14500)
elif isinstance(result, (int, float)):
value = int(result)
else:
value = 14500
print(int(value))
if __name__ == "__main__":
main()
This sets the export limit. It includes retry logic and verification because the FoxESS API can occasionally fail silently (more on that in Lessons Learned).
#!/usr/bin/env python3
"""Set FoxESS export limit via Cloud API.
Usage: python3 foxess_set_export.py <watts>
"""
import sys
import io
import time
import requests
_real_stdout = sys.stdout
sys.stdout = io.StringIO()
try:
import foxesscloud.openapi as f
finally:
sys.stdout = _real_stdout
API_KEY = "YOUR_API_KEY_HERE"
DEVICE_SN = "YOUR_DEVICE_SERIAL_HERE"
# Home Assistant connection (for notifications on failure)
HA_TOKEN = "YOUR_HA_LONG_LIVED_TOKEN"
HA_URL = "http://localhost:8123"
HA_HEADERS = {"Authorization": f"Bearer {HA_TOKEN}", "Content-Type": "application/json"}
MAX_RETRIES = 3
RETRY_DELAY_SECONDS = 10
VERIFY_DELAY_SECONDS = 5
def verify_export_limit(expected_watts):
"""Read back the export limit from the inverter and check it matches."""
try:
result = f.get_named_settings("ExportLimit")
if isinstance(result, (int, float)):
return int(result) == expected_watts
elif isinstance(result, dict):
val = result.get("value", result.get("ExportLimit"))
if val is not None:
return int(val) == expected_watts
return False
except Exception as e:
print(f" Warning: could not verify export limit: {e}", file=sys.stderr)
return False
def ha_notify(title, message, notification_id=None):
"""Send a persistent notification to Home Assistant on failure."""
try:
data = {"title": title, "message": message}
if notification_id:
data["notification_id"] = notification_id
requests.post(
f"{HA_URL}/api/services/persistent_notification/create",
headers=HA_HEADERS,
json=data,
timeout=5,
)
except Exception:
pass
def set_export_with_retry(watts):
"""Set export limit with retry logic and verification."""
for attempt in range(1, MAX_RETRIES + 1):
print(f" Attempt {attempt}/{MAX_RETRIES}: Setting ExportLimit to {watts}W")
try:
f.set_named_settings("ExportLimit", watts)
except Exception as e:
print(f" ERROR on attempt {attempt}: {e}", file=sys.stderr)
if attempt < MAX_RETRIES:
time.sleep(RETRY_DELAY_SECONDS)
continue
time.sleep(VERIFY_DELAY_SECONDS)
if verify_export_limit(watts):
print(f" VERIFIED: ExportLimit confirmed at {watts}W")
return True
else:
print(f" VERIFICATION FAILED on attempt {attempt}", file=sys.stderr)
if attempt < MAX_RETRIES:
time.sleep(RETRY_DELAY_SECONDS)
# All retries exhausted
print(f" CRITICAL: Failed to set ExportLimit after {MAX_RETRIES} attempts!", file=sys.stderr)
ha_notify(
title="EXPORT LIMIT FAILED",
message=f"Failed to set FoxESS export limit to {watts}W after {MAX_RETRIES} attempts.",
notification_id="foxess_export_limit_failed",
)
return False
def main():
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <watts>", file=sys.stderr)
sys.exit(1)
try:
watts = int(sys.argv[1])
except ValueError:
print(f"Error: '{sys.argv[1]}' is not a valid integer", file=sys.stderr)
sys.exit(1)
if watts < 0 or watts > 14500:
print(f"Error: watts must be between 0 and 14500, got {watts}", file=sys.stderr)
sys.exit(1)
old_stdout = sys.stdout
sys.stdout = io.StringIO()
try:
f.api_key = API_KEY
f.device_sn = DEVICE_SN
f.get_device()
finally:
sys.stdout = old_stdout
print(f"Setting ExportLimit to {watts}W...")
verified = set_export_with_retry(watts)
if not verified:
sys.exit(1)
if __name__ == "__main__":
main()
Run these manually to confirm they work before wiring up automations:
# Read the current export limit
docker exec homeassistant python3 /config/scripts/foxess_get_export.py
# Set export to 5000W (safe, only changes the export cap)
docker exec homeassistant python3 /config/scripts/foxess_set_export.py 5000
# Reset back to maximum
docker exec homeassistant python3 /config/scripts/foxess_set_export.py 14500
Add these blocks to your configuration.yaml. This gives you the slider, the sensor that polls the current limit, and the shell commands that the automations will call.
command_line:
- sensor:
name: "FoxESS Export Limit Reading"
unique_id: foxess_export_limit_reading
command: "python3 /config/scripts/foxess_get_export.py"
unit_of_measurement: "W"
command_timeout: 30
scan_interval: 300
input_number:
foxess_export_limit:
name: FoxESS Export Limit
icon: mdi:transmission-tower-export
min: 0
max: 14500
step: 500
unit_of_measurement: "W"
mode: slider
input_boolean:
globird_peak_export_schedule:
name: GloBird Peak Export Schedule
icon: mdi:clock-check
shell_command:
set_foxess_export_limit: >
python3 /config/scripts/foxess_set_export.py {{ states('input_number.foxess_export_limit') | int(14500) }}
set_foxess_export_limit_14500: >
python3 /config/scripts/foxess_set_export.py 14500
After adding these, restart Home Assistant or reload the config (Developer Tools > YAML > Reload All).
You need three automations. You can add these via the HA UI (Settings > Automations > Create) or directly in automations.yaml.
This fires 5 minutes before the Super Export window. It reads whatever you’ve set on the slider and pushes it to the inverter.
- id: globird_peak_export_start
alias: "FoxESS Set Export from slider (5:55 PM)"
description: >
Set FoxESS export limit from the slider value at 5:55 PM for the GloBird
super export window.
triggers:
- trigger: time
at: "17:55:00"
conditions:
- condition: state
entity_id: input_boolean.globird_peak_export_schedule
state: "on"
actions:
- action: shell_command.set_foxess_export_limit
mode: single
After the window closes, reset everything back to full export and update the slider to match.
- id: globird_peak_export_end
alias: "FoxESS Reset Export 14.5kW (9:05 PM)"
description: >
Reset FoxESS export limit to 14500W at 9:05 PM after the super export window.
Also resets the slider so the UI reflects the actual state.
triggers:
- trigger: time
at: "21:05:00"
conditions:
- condition: state
entity_id: input_boolean.globird_peak_export_schedule
state: "on"
actions:
- action: input_number.set_value
target:
entity_id: input_number.foxess_export_limit
data:
value: 14500
- action: shell_command.set_foxess_export_limit_14500
mode: single
If you adjust the slider at any time (say you want to tweak it before the window starts), this pushes the new value to the inverter straight away.
- id: foxess_push_slider_change
alias: "FoxESS Push Export Limit (slider changed)"
description: >
When the export limit slider is changed in the UI, immediately push the new
value to the inverter via the Cloud API.
triggers:
- trigger: state
entity_id: input_number.foxess_export_limit
conditions:
- condition: template
value_template: "{{ trigger.from_state.state != trigger.to_state.state }}"
actions:
- action: shell_command.set_foxess_export_limit
mode: single
Your inverter should be configured with a ForceDischarge schedule covering the 6pm to 9pm window. This tells the battery to actively push power to the grid. The export limit then caps how fast it can export.
| Time | What Happens |
|---|---|
| During the day | Solar charges your battery to 100% (plus free grid charging during ZEROCHARGE) |
| 5:55 PM | Automation sets export limit from slider (e.g., 5000W) |
| 6:00 PM | ForceDischarge kicks in, battery pushes to grid at up to 5kW |
| 9:00 PM | ForceDischarge ends |
| 9:05 PM | Automation resets export limit to 14500W, slider resets |
The ForceDischarge/ForceCharge schedules should be set up by FoxESS support through the app or done once manually. Don’t automate those (see Lessons Learned for why).
| API Call | Safe? | What It Does |
|---|---|---|
get_named_settings("ExportLimit") | Yes | Reads current export limit |
set_named_settings("ExportLimit", watts) | Yes | Sets export limit only |
get_schedule() | Yes | Reads the full schedule (read only) |
set_schedule(...) | NO | Replaces the entire inverter schedule. Don’t use this. |
If you want visibility into what the scripts are doing, add an input_text entity to your config:
input_text:
foxess_script_status:
name: FoxESS Script Status
icon: mdi:script-text
max: 255
Then add a few lines to your set script to post status updates via the HA REST API. This gives you a logbook entry every time the export limit changes, which is handy for debugging.
The fixed 5000W approach works great, but it’s a bit dumb. It doesn’t account for:
The next evolution is a dynamic controller that adjusts the export limit every 15 minutes during the 6pm to 9pm window. The logic goes something like this:
Every 15 minutes between 6pm and 9pm:
The whole setup is: two Python scripts, one slider, one toggle, three automations. The scripts talk to the FoxESS Cloud API to control export rate only. The automations handle the timing around the GloBird Super Export window.
Set the slider to 5000W, turn on the toggle, and you’ll reliably export 15 kWh at the premium rate every evening. If you want to get fancy later, the dynamic export controller is the next step.