Skip to content

Exec Mode

ha-repl exec runs one snippet of Python against a running Home Assistant and exits. It is the non-interactive form of live mode, and its main use is giving a coding agent a way to check its ideas against a real instance: read an entity's state, query the recorder, call a service, inspect an object, then decide what to do next.

It needs the same server component as live mode - read the warnings on the live mode page first: an agent with exec can run any Python inside Home Assistant. Point it at a devcontainer or development instance, not the one running your house.

Setup

The agent needs the ha-repl command and two environment variables:

export HASS_SERVER=http://homeassistant.local:8123
export HASS_TOKEN=<long lived access token>

Both can also go in a .env file in the directory the agent runs from. Then tell the agent the command exists, for example in its project instructions:

Use `ha-repl --json exec '<python>'` to run Python against the development
Home Assistant. `hass` is the live HomeAssistant object, `obj` the object tree,
`sql("select ...")` queries the recorder. See docs/exec_mode.md.

Running a Snippet

Pass the code as an argument, from a file, or on standard input. Standard input avoids shell quoting problems and is the best form for anything longer than a line.

ha-repl exec 'hass.states.get("sun.sun").state'
ha-repl exec -f snippet.py
ha-repl exec - <<'PY'
r = sql("select entity_id from states_meta", max_rows=3)
{row[0]: hass.states.get(row[0]).state for row in r}
PY

The value of the last expression is printed, as at a Python prompt. await works at the top level, and a call to an async function is awaited for you if you leave the await out.

Option Effect
--json Print one JSON object instead of text. Goes before exec
-s NAME, --session NAME Use a named session inside Home Assistant (default default). Goes before exec
-t SECONDS, --timeout SECONDS Give up after this long
--reset Clear the session inside Home Assistant before running
-f FILE Read the snippet from a file

The exit status is 0 on success, 1 if the snippet raised or timed out, and 2 if Home Assistant could not be reached or the arguments were wrong.

JSON Output

With --json the result is one object on standard output, whether or not the snippet succeeded:

ha-repl --json exec 'sql("select * from states_meta", max_rows=2)'
{
  "stdout": "",
  "value": {
    "columns": ["metadata_id", "entity_id"],
    "rows": [[1, "zone.home"], [2, "conversation.home_assistant"]],
    "rowcount": 2,
    "truncated": true
  },
  "error": null,
  "duration": 0.004,
  "truncated": false
}
Field Content
stdout Everything the snippet printed
value The last expression, or null if the snippet ended in a statement
error null, or an object with type, message and traceback
duration Seconds the snippet took
truncated true if output from inside Home Assistant was cut at its size limit

What value holds depends on where the last expression ran:

  • A sql result is its data: columns, rows (one list per row), rowcount, and its own truncated, which is true when the query hit max_rows and there were more rows to fetch.
  • Any other local value that is already JSON-shaped (numbers, strings, lists, dicts) is passed through as it is. Anything else is its repr().
  • A value from inside Home Assistant is always a string holding its printed form, so hass.states.get("sun.sun").state gives "'below_horizon'". To get real data back, print(json.dumps(...)) inside the snippet and read stdout, or shape the answer into plain data and keep the final step local.

What Runs Where

A snippet is ordinary Python running in the ha-repl process, with whatever is installed there. Statements that use hass or obj are the exception: they are sent to Home Assistant and run inside it. The rules are the same as the interactive shell and are described in full under What Runs Where. The points that matter most for a script:

  • sql and hass_api are local. A query's result is downloaded, and everything done with it afterwards happens locally.
  • Plain local data (strings, numbers, lists and dicts of them) and sql results can be used in a statement that runs inside Home Assistant. A sql result arrives there as a list of rows.
  • Nothing comes back the other way. A variable assigned by a statement that used hass or obj stays inside Home Assistant, and later statements that use it run there too.
  • Don't call sql(...) in the same statement as hass or obj. Assign the result on its own line first.

State Between Calls

Each exec call starts a new local Python process, so local variables and imports do not carry over. Variables inside Home Assistant do: they live in the named session until it is reset or Home Assistant restarts.

ha-repl exec 'entry = hass.config_entries.async_entries("sun")[0]'
ha-repl exec 'entry.state'          # still there
ha-repl sessions                    # list sessions and their variables
ha-repl reset                       # clear the default session

Give each agent, or each task, its own --session name if several run against the same instance, so they don't overwrite each other's variables.

Errors

Without --json, a traceback goes to standard error and the exit status is 1. With --json, error is filled in and stdout still holds whatever was printed before the failure:

{
  "stdout": "",
  "value": null,
  "error": {
    "type": "ZeroDivisionError",
    "message": "division by zero",
    "traceback": "Traceback (most recent call last):\n  File \"/ha_repl/api_cell_1\", line 1, in <module>\n    x = 1/0\n        ~^~\nZeroDivisionError: division by zero\n"
  },
  "duration": 0.001,
  "truncated": false
}

A snippet stops at the first statement that fails. Statements before it have already run, including any that changed something inside Home Assistant.

For an error raised inside Home Assistant, type and message are plain, but traceback is formatted for a terminal with box-drawing characters. Rely on type and message when parsing.

A rejected query raises SqlError. Only a single SELECT statement is accepted.

Tips for Agents

  • Use --json, and send the snippet on standard input.
  • Always set -t. A snippet that waits on something that never happens otherwise blocks forever.
  • Keep max_rows small while exploring. The default is 1000 rows; sql.tables lists the tables and their columns without running a query.
  • Prefer reading to writing. hass.states.get(...), obj[...] and sql(...) are safe to repeat; service calls and direct changes to hass objects take effect immediately on the instance.
  • help(thing) on an object inside Home Assistant prints a summary of its methods and properties, which is often quicker than reading the source.