Sign inSign up

bmach/ovo-agent

By bmach

•Updated about 1 month ago

Automates downloading your OVO Energy (AU) usage data and imports it into Home Assistant

Image
0

2.9K

bmach/ovo-agent repository overview

⁠OVO Agent

Automates downloading your OVO Energy (Australia) usage data and importing it into Home Assistant as long-term energy statistics. Runs on a schedule — no manual exports, no spreadsheets.

Pipeline: logs into OVO → requests a usage data export → polls your email for the download link → downloads the CSV → archives the email → splits it into daily files → imports into Home Assistant → cleans up old files per your retention policy.

Supports linux/amd64 and linux/arm64 (Raspberry Pi).

Tags: latest tracks the newest release. Pin a specific version (e.g. 1.0.2) in production for predictable upgrades — see CHANGELOG.md⁠ for what's changed between versions before upgrading.


⁠Quick start

Create a working directory and a .env file:

mkdir -p /opt/ovo-agent/data
cd /opt/ovo-agent

docker-compose.yml:

services:
  ovo-agent:
    image: bmach/ovo-agent:latest
    container_name: ovo-agent
    restart: unless-stopped
    pull_policy: always
    env_file:
      - .env
    environment:
      CSV_DOWNLOAD_DIR: /data
      COOKIES_FILE: /data/ovo_cookies.json
    volumes:
      - ./data:/data
    command: ovo-schedule

.env (minimum required):

[email protected]
OVO_PASSWORD=your_ovo_password

IMAP_HOST=imap.gmail.com
[email protected]
IMAP_PASSWORD=xxxx xxxx xxxx xxxx     # Gmail App Password, see below
IMAP_ARCHIVE=[Gmail]/All Mail

HA_URL=http://homeassistant.local:8123
HA_TOKEN=your_long_lived_access_token

HA_TIMEZONE=Australia/Brisbane        # also controls what timezone CRON_SCHEDULE runs in
CRON_SCHEDULE=0 4 * * *               # 4am in HA_TIMEZONE, not UTC

Start it:

docker compose up -d
docker compose logs -f

That's it — the container will check for a new usage export every day at the scheduled time and keep Home Assistant up to date.


⁠Gmail App Password

If your inbox is Gmail, you need an App Password (your normal password won't work with 2FA enabled):

  1. Go to https://myaccount.google.com/apppasswords⁠
  2. Create one for Mail → name it "OVO Agent"
  3. Use the 16-character password as IMAP_PASSWORD

Other IMAP providers (Outlook, Fastmail, etc.) work too — just set IMAP_HOST accordingly.

⁠Home Assistant long-lived token

  1. HA → your Profile → Security → Long-lived access tokens
  2. Create one named "OVO Agent"
  3. Use it as HA_TOKEN

No YAML configuration is needed in Home Assistant — the container creates the energy sensors automatically the first time it imports data:

Energy (always imported):

SensorMeaning
sensor.ovo_grid_consumptionGrid import (hourly kWh)
sensor.ovo_solar_exportSolar export to grid (hourly kWh)
sensor.ovo_control_load_2Control Load 2 (zero until an appliance is connected)

Cost statistics — created automatically when tariffs.yaml is present alongside .env:

SensorMeaning
sensor.ovo_grid_costCumulative AUD grid consumption cost (TOU-aware)
sensor.ovo_solar_export_earningsCumulative AUD solar feed-in credit
sensor.ovo_supply_chargeCumulative AUD daily supply charge

Cost sensors are best used in ApexCharts or statistics graph cards. They do not appear in the Energy Dashboard cost picker (HA limitation with imported statistics — see README for details).

Cost (imported when tariffs.yaml is present):

SensorMeaning
sensor.ovo_grid_costCumulative grid consumption cost (AUD)
sensor.ovo_solar_export_earningsCumulative solar feed-in credit (AUD)
sensor.ovo_supply_chargeCumulative daily supply charge (AUD)

Add kWh sensors to Settings → Energy once the first import has run. Cost statistics work in ApexCharts and Statistics cards. See the project README for wiring the Energy Dashboard cost column.

⚠️ Don't create your own template sensors for these entities in HA YAML — it causes a statistics conflict. The container manages these sensors entirely on its own.


⁠Running other commands

The default command (ovo-schedule) runs the daemon. To run any other task as a one-off, throwaway container:

docker compose run --rm ovo-agent <command>

Useful examples:

# Run the full pipeline once, right now (don't wait for the schedule)
docker compose run --rm ovo-agent ovo-agent

# Test your IMAP/email setup without touching OVO
docker compose run --rm ovo-agent ovo-imap-test --since-minutes 1440 --dry-run

# Re-split the most recent download sitting in your data folder
docker compose run --rm ovo-agent ovo-split

# Preview what the next import would do, without writing to HA
docker compose run --rm ovo-agent ovo-ha-import --dry-run

# Force a full clean reimport of all historical data
docker compose run --rm ovo-agent ovo-ha-import --clear --reimport

# Apply your retention policy manually
docker compose run --rm ovo-agent ovo-archive --dry-run

⁠Data persistence

Everything lives under the mounted ./data volume:

data/
├── raw/              full downloads (kept for RAW_RETAIN_DAYS, default 2)
├── daily/             per-day CSVs (kept for DAILY_RETAIN_DAYS, default 90)
├── errors/            browser debug screenshots (kept for ERRORS_RETAIN_DAYS, default 30 days)
│                      each file prefixed YYYY-MM-DD_HHMMSS_ for chronological history
├── import_log.json    tracks what's already in Home Assistant
└── ovo_cookies.json   browser session, avoids logging in every run
└── tariffs.yaml       user data to enable cost statistics. 

A sample tariffs.yaml file is included in the project repository. Run docker compose run --rm ovo-agent ovo-tariff-test to verify rate resolution before your first import.

Back up this folder if you want to preserve session state and import history across container recreation — though everything can also be safely rebuilt by reimporting from OVO if lost.

If tariffs.yaml is missing, cost statistics are silently skipped.


⁠Common environment variables

VariableDefaultPurpose
CRON_SCHEDULE0 4 * * *When the daily pipeline runs — interpreted in HA_TIMEZONE, not UTC
HA_TIMEZONEUTCYour local timezone — also used to schedule CRON_SCHEDULE correctly
MAX_SPLIT_DAYS90How far back to process on each run
RAW_RETAIN_DAYS2How many full downloads to keep
DAILY_RETAIN_DAYS90How many daily CSVs to keep
ERRORS_DIRCSV_DOWNLOAD_DIR/errorsWhere browser failure screenshots are saved
ERRORS_RETAIN_DAYS30How many days to keep debug screenshots
TARIFFS_FILE<project_root>/tariffs.yamlPath to tariff config — omit file to skip cost import
EMAIL_POLL_TIMEOUT600Seconds to wait for the OVO email
RETRYdisabledSet to enabled to schedule one delayed retry when no new dates are available
RETRY_HOURS2Hours to wait before the one no-new-data retry
LOG_LEVELINFOSet to DEBUG for troubleshooting

Tariff rates and plan history are configured in tariffs.yaml (not .env) — see the project README for the full format.

⚠️ The container's system clock defaults to UTC regardless of your host's timezone. If HA_TIMEZONE isn't set, CRON_SCHEDULE runs against UTC — "4am" will fire at 4am UTC, not 4am where you live. Always set HA_TIMEZONE to your local IANA timezone (e.g. Australia/Brisbane, America/New_York).

Full configuration reference and source: see the project README.


⁠Troubleshooting

SymptomFix
Login failsSet LOG_LEVEL=DEBUG and check ./data/errors/ for a timestamped screenshot
Email never foundConfirm IMAP_USER / IMAP_PASSWORD are correct and OVO_EMAIL_SENDER matches [email protected]
"Mean type has changed" in HARemove any custom YAML sensors for sensor.ovo_*, then run ovo-ha-import --clear --reimport
CSV download fails (403)The signed link expired (6h limit) — increase EMAIL_POLL_TIMEOUT or run more frequently
Scheduled run fires at the wrong timeHA_TIMEZONE not set — defaults to UTC, not your local time

For full documentation, configuration options, and source code, see the project repository.

⁠Changelog

All notable changes to this project are documented here. Format follows Keep a Changelog⁠, and this project uses Semantic Versioning⁠.

⁠[Released]

⁠[1.1.3] - 2026-09-06

⁠Changed
  • Login failures now retry through a staged schedule: after 1 minute, after 30 minutes, and after LOGIN_RETRY_HOURS (default 2 hours), before hard failure. This applies to unexpected login failures as well as known OVO outage and form-processing errors.
  • Added LOGIN_RETRY_INITIAL_DELAY_SECONDS, LOGIN_RETRY_SECOND_DELAY_SECONDS, and LOGIN_RETRY_HOURS configuration settings. LOGIN_RETRY_ATTEMPTS remains available and defaults to 4.

⁠[1.1.2] - 2026-08-04

⁠Fixed
  • Login no longer relies on wait_for_url() to detect a successful login. OVO's login form is served from the same domain as the rest of the app, so the URL already matched the "success" pattern before the form was even submitted — meaning a transient OVO login-form error ("There was an error processing your request…") was silently treated as a successful login, and only surfaced two steps later as "Usage tab not found". Login success is now confirmed the same way is_logged_in() does it: by waiting for the Usage tab in the authenticated app shell.
⁠Added
  • Login now retries in-session on two recurring OVO-side transient errors — a full-page outage screen ("Oops!, something went wrong") and the login form error above — up to LOGIN_RETRY_ATTEMPTS times (default 3), waiting LOGIN_RETRY_DELAY_SECONDS between attempts (default 300s / 5 min). This is separate from the existing no-new-data retry: that one waits hours and solves a different problem (OVO not having exported data yet), whereas a login blip clears up within minutes and shouldn't delay the whole run. Unrecognised login failures (e.g. selectors changed, real credential failure) are not retried.

⁠[1.1.1] - 2026-07-13

⁠Fixed
  • Home Assistant statistics imports now include explicit mean_type metadata and unit_class for AUD cost statistics, avoiding recorder deprecation warnings ahead of HA Core 2026.11.

⁠[1.1.0] - 2026-07-09

⁠Added
  • Added optional no-new-data retry scheduling for the long-running scheduler. Set RETRY=enabled in .env to retry once when a run finds no new dates to import; RETRY_HOURS controls the delay and defaults to 2 hours.
⁠Changed
  • When retry is enabled, the initial no-new-data run no longer sends a Home Assistant notification. The notification is only sent if the single retry also finds no new dates.

⁠[1.0.5] - 2026-06-26

⁠Added
  • Now sends a notification to HA if there is no new dates to input on a run as sometimes OVO must have some data delays.

⁠[1.0.4] - 2026-06-24

⁠Added
  • tariff.py — tariff engine that loads tariffs.yaml and resolves the correct AUD rate for any 5-minute interval, supporting multiple plans with date ranges, time-of-use windows, solar feed-in credits, and daily supply charges.
  • tariffs.yaml — static tariff configuration file placed in download folder. Supports any number of plans; the active plan is detected by a null end date. Rate windows are defined as local-time start times (HH:MM). Missing file is gracefully skipped with an info log — no code changes required to enable or disable cost import.
  • Cost statistics imported into HA alongside kWh data (when tariffs.yaml is present):
    • sensor.ovo_grid_cost — cumulative AUD cost of grid consumption (E1), calculated per 5-minute interval using the applicable TOU window rate
    • sensor.ovo_solar_export_earnings — cumulative AUD credit for solar export (B1) at the configured feed-in tariff rate
    • sensor.ovo_supply_charge — cumulative AUD daily supply charge, apportioned per day from supply_charge_daily (cents/day) in the plan
  • ovo-tariff-test CLI entrypoint — verifies tariff resolution across all time-of-use windows for today (default) or a specific datetime. Useful for confirming plan transitions and window boundaries before a full import.
  • pyyaml dependency added for tariffs.yaml parsing.
  • ovo-tariff-test added to launch.json with date/time prompt inputs.
⁠Changed
  • ha_importer.py --clear now also clears the three cost statistics (sensor.ovo_grid_cost, sensor.ovo_solar_export_earnings, sensor.ovo_supply_charge) alongside the kWh statistics.
  • Missing tariffs.yaml logs at INFO level (not WARNING) since it is a valid opt-out state, not an error.

⁠[1.0.3] - 2026-06-23

⁠Fixed
  • Browser error screenshots were saved to /tmp which is ephemeral in Docker and overwritten on every run. Screenshots now save to CSV_DOWNLOAD_DIR/errors/ (inside the mounted data volume) with a YYYY-MM-DD_HHMMSS_ datetime prefix so failures are persistent and historical across cron runs.
  • Archiver was duplicating os.getenv() calls for retention policy values instead of reading from config.py, creating two sources of truth for defaults that could silently drift. All retention constants now live exclusively in config.py.
  • ERRORS_DIR had no consistent default in the archiver — it relied on config.ERRORS_DIR by accident rather than by design.
⁠Added
  • ERRORS_DIR config constant in config.py (defaults to CSV_DOWNLOAD_DIR/errors/) for browser debug screenshots.
  • RAW_RETAIN_DAYS, DAILY_RETAIN_DAYS, and ERRORS_RETAIN_DAYS retention constants moved to config.py as the single source of truth.
  • apply_errors_retention() in archiver.py — deletes debug screenshots older than ERRORS_RETAIN_DAYS days (default 30) as part of the standard archival pipeline.

⁠[1.0.2] - 2026-06-21

⁠Fixed
  • Scheduler now interprets CRON_SCHEDULE in HA_TIMEZONE instead of the container's system clock (which defaults to UTC). Previously a schedule of 0 4 * * * would fire at 4am UTC, not 4am local time.
  • Added TZ environment variable support in docker-compose.yml for OS-level timezone consistency (log timestamps, file mtimes).
⁠Changed
  • docker-compose.yml now pins a specific image version by default instead of tracking latest, for predictable upgrades and easy rollback.

⁠[1.0.1] - 2026-06-20

⁠Fixed
  • Dockerfile: README.md was missing from the builder stage's build context, causing pip install . to fail during multi-arch image builds (Poetry's build backend requires the file referenced in pyproject.toml's readme field to be present).
  • HA importer: corrected cumulative statistics sum calculation across separate import runs — previously each run's sum reset to zero instead of continuing from the last known value in Home Assistant, causing incorrect (including negative) energy readings at day boundaries.
⁠Added
  • .dockerignore to exclude local development files from the build context.

⁠[1.0.0] - 2026-06-19

⁠Added
  • Initial release.
  • Browser automation (Playwright) for OVO Energy AU login and usage data export request.
  • IMAP polling for the OVO download email, with CSV extraction and email archiving.
  • CSV splitter: breaks the full OVO history export into per-day files, with configurable lookback window (MAX_SPLIT_DAYS) and automatic skipping of already-imported dates.
  • Home Assistant integration via the WebSocket recorder/import_statistics API — imports hourly energy statistics directly into HA's long-term statistics database. No YAML configuration required.
  • Retention policy archiver for raw downloads and daily CSV files.
  • Cron-based scheduler daemon.
  • Multi-arch Docker image (linux/amd64, linux/arml64) with a multi-stage build that excludes Poetry and build tools from the final image.

Tag summary

Content type

Image

Digest

sha256:52f8d665b…

Size

451.2 MB

Last updated

about 1 month ago

docker pull bmach/ovo-agent