esphome
Build and OTA-flash ESPHome nodes through the containerized esphome CLI on the home-automation
host. espctl is a thin wrapper: node builds/flashes run via docker exec over SSH; the dashboard
is polled directly only for a quick version reachability check. Node YAMLs live on the host in the
directory bind-mounted to /config in the container.
Container assumption
This drives a Docker container named esphome (override with $ESPHOME_CONTAINER) — typically
a service in the host's docker-compose/compose.yaml, with the host's ESPHome config dir mounted
to /config. Every command shells docker exec <container> esphome … over SSH; there is no bare
local ESPHome install in the loop. The dashboard normally listens on :6052. For OTA to work, the
ESPHome service must bind the LAN IP (--address in the compose service), and exposure of :6052
should be restricted to the LAN.
Setup (once)
- On the host: run the
esphomecontainer (compose service namedesphome) with the config dir mounted to/config, and ensure key-based SSH access to the host (the tool usesBatchMode=yes— no password prompt). - In the overlay config: the connection facts live in the committed overlay at
<overlay>/config/home-assistant.vars(path via$AXON_HOME_ROOT, default the active deployment overlay):ESPHOME_SSH(an ssh target such asuser@host),ESPHOME_URL(the dashboard base URL, e.g.http://host:6052), andESPHOME_CONTAINER(the container name). All three are non-secret and committed there; each can be overridden per-run via the matching$ESPHOME_*env var (env wins over the config file). - No secret for this skill. It reaches the container over SSH with key auth — there is no password or token, so there is no Bitwarden item to provision. (If a secret is ever needed later, add a bw fetch then; today there is none.)
Run
The tool reads its connection facts from the overlay config — no unlock step:
scripts/espctl version # dashboard reachability
scripts/espctl nodes # list node YAMLs in the container
scripts/espctl compile bedroom # compile bedroom.yaml (no flash)
scripts/espctl upload bedroom # OTA-flash bedroom.yaml over WiFi
scripts/espctl logs bedroom # stream logs (Ctrl-C to stop)
scripts/espctl run bedroom # compile + upload + tail logs
Workflow for a new node
- Add
<node>.yaml(plus asecrets.yamlon the host, not committed) to the host's ESPHome config dir. - Sync it to the host / into the
/configmount. - First flash is over USB on the machine the device is plugged into; subsequent flashes are OTA
via
upload.compile/uploadaccept the node name with or without the.yamlsuffix.
Gotchas
- The
esphomecontainer must be running and reachable viadocker execon the SSH host —nodesreturns an empty list (not an error) when the container is up but no non-secretsYAMLs exist yet. - Connection facts come from the overlay config. If a command dies with a missing/placeholder error,
fill
ESPHOME_SSH/ESPHOME_URL/ESPHOME_CONTAINERin<overlay>/config/home-assistant.vars(or set the matching$ESPHOME_*env var). ATODO-placeholder is treated as unset. - No secret / no
bw. This skill uses SSH key auth, so there is no Bitwarden item and no$BW_SESSIONto unlock — SSH key access to the host is the only credential. - OTA needs the LAN IP bound. If
uploadcannot reach the device, confirm the ESPHome service binds the LAN IP (--addressin the compose service) rather than a container-internal address. compilecan take minutes on a cold build (SSH command timeout is 900s);logs/runstream until Ctrl-C.
Examples
"Flash the bedroom node over the air."
→ espctl upload bedroom → compiles then OTA-flashes bedroom.yaml; tail with espctl logs bedroom.
"Is the ESPHome dashboard up?"
→ espctl version → prints dashboard: <version> or a clear unreachable error at ESPHOME_URL.
"What nodes are configured?"
→ espctl nodes → lists the *.yaml node files in the container's /config (excluding secrets).