FoxESS + Home Assistant

Jul 11, 2026

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:

  • A Python script that talks to the FoxESS Cloud API to set/read your export limit
  • A slider in Home Assistant to control the limit
  • Automations that cap export before the window starts and reset it after

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:

  1. Capping the export rate so you pace yourself to exactly 15 kWh over the 3 hour window
  2. Giving you a slider in the UI so you can adjust on the fly (cloudy day, guests over, etc.)
  3. Automatically resetting after the window so your system returns to normal operation
  4. Verifying the API call actually worked (the FoxESS API occasionally lies about success)
  5. Alerting you if something fails so you can intervene manually

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:

TimeBattery SoCWhat’s Happening
6:00 AM52%Overnight drain done, solar starting to trickle in
8:00 AM48%Morning loads (heating, kettle, etc.) pulling from battery
11:00 AM45%ZEROCHARGE window opens, ForceCharge kicks in
1:55 PM100%Battery full from free grid charging
5:55 PM100%HA sets export limit to 5000W
6:00 PM99%ForceDischarge starts, exporting at ~5kW
7:30 PM78%Midway through window, on track
9:00 PM58%Window ends, ~15 kWh exported
9:05 PM58%HA resets export limit to 14500W
11:00 PM52%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.

  • FoxESS hybrid inverter (KH series or similar with Cloud API access)
  • Home Assistant running in Docker (or any install, the config is the same)
  • A FoxESS Cloud API key
  • The foxesscloud Python library installed inside your HA container
  • GloBird ZEROHERO plan (or similar time-of-use tariff where you want to control export)
  1. Log into the FoxESS Cloud portal (https://www.foxesscloud.com)
  2. Go to your profile/settings and generate an API key
  3. Find your inverter’s serial number (it’s on the device label and also shown in the portal under your plant)

You’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.

TimeWhat Happens
During the daySolar charges your battery to 100% (plus free grid charging during ZEROCHARGE)
5:55 PMAutomation sets export limit from slider (e.g., 5000W)
6:00 PMForceDischarge kicks in, battery pushes to grid at up to 5kW
9:00 PMForceDischarge ends
9:05 PMAutomation 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 CallSafe?What It Does
get_named_settings("ExportLimit")YesReads current export limit
set_named_settings("ExportLimit", watts)YesSets export limit only
get_schedule()YesReads the full schedule (read only)
set_schedule(...)NOReplaces 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:

  • How much battery you actually have left
  • How much you’ve already exported tonight
  • Whether you’ll have enough battery to survive overnight until the next ZEROCHARGE window
  • Days where you started with less than 100% (cloudy day, high daytime usage)

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:

  1. Check current battery SoC
  2. Check how many kWh you’ve already exported this window (from the HA energy dashboard or a utility meter sensor)
  3. Calculate how much battery headroom you have after reserving enough for overnight (you need roughly 30 to 40% to survive from 9pm through to 11am the next day with heating loads)
  4. Calculate the ideal export rate to land at exactly 15 kWh by 9pm
  5. Push the new limit to the inverter

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.