# Home Assistant REPL > Python REPL Shell for Home Assistant Developers Home Assistant REPL is a Python shell for developers of Home Assistant custom components. It exposes the live object tree - entities and more - as a dict-like `obj`, for exploring APIs, trying out code snippets and hotfixing issues directly against a running Home Assistant instance. `api` mode uses only the standard Home Assistant HTTP/WebSocket API; an optional `live` mode, via a HACS companion component, adds direct access to the core Python API as `hass`. # Start here # Home Assistant REPL A REPL shell custom designed for Home Assistant custom component developers, tinkerers and native Python speakers. Makes it easy as pie! It is an opinionated REPL ([Read-Eval-Print-Loop](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop)) shell that aims are to make it easier without any configuration to: - Exploring of the APIs in context of a live working instance - Trialling out snippets of code - Debugging code (but see note below) - Hotfixing issues that don't have built in support to do so from existing components. uv run --with homeassistant-repl ha-repl --token=Home Assistant REPL (API client mode) connected to ws://homeassistant.local:8123/api/websocket. `obj` only, read-only - no `hass`. Ctrl-D to exit.obj["/mqtt/binary_sensor/kitchen_terrace_window_tilt"].state'off'\[o.name for o in obj["/rflink/binary_sensor"].values() if o.state=='unavailable'\]['Shed Intruder Alarm', 'Panic Keyfob']{(o.entity_id, o.state) for o in obj.find(domain="binary_sensor",platform="mqtt")}{\ ('binary_sensor.terrace_pir_occupancy', 'unavailable'),\ ('binary_sensor.scullery_smoke_alarm_battery_low', 'off'),\ ('binary_sensor.kitchen_terrace_window_tweaked', 'off'),\ ('binary_sensor.scullery_water_detector_water_leak', 'dry'),\ ('binary_sensor.boiler_co_detector_battery_low', 'off'),\ ('binary_sensor.pantry_sensor_occupancy', 'off')\ } > > > If you're not already comfortable using Python tools to manipulate data on the fly, or better REPL shells in other languages, this is a great way to learn, and faster at the keyboard than clicking around Jupyter notebooks. This is primarily for developers of custom components, and their LLM agents, though may be of interest for other folk tinkering with Home Assistant. It is a potentially sharp tool, so NOT appropriate for general Home Assistant users. ## Features - Integrated with `rich` and `pygments` for pretty object printouts and stack traces - Reuses session and history features from `prompt-toolkit` - Access to all entities via dictionary like interface, `obj` - Usual multi-line editing support and history of Python - Integrated with [homeassistant-api](https://pypi.org/project/HomeAssistant-API/) as `hass_api` object - Auto-awaits coroutines for easy shell use (can be switched off or overridden) - Dedicated shell that can be run without installation with `uv` - Add in your own [plugins](https://homeassistant-repl.rhizomatics.org.uk/configuration/plugins/index.md) or package up a plugin with your component to help other developers - Usable from inside `ipython` shell, Marimo notebooks or plain `python -m asyncio` All of the above works with standard Home Assistant APIs, referred to as [`api` mode](https://homeassistant-repl.rhizomatics.org.uk/modes/api_mode/index.md). Home Assistant REPL also has an advanced [`live` mode](https://homeassistant-repl.rhizomatics.org.uk/modes/live_mode/index.md) that taps directly into a live Home Assistant using an optional server component available via [HACS](http://hacs.xyz). Get started with a single line at [Quick Start](https://homeassistant-repl.rhizomatics.org.uk/quick_start/index.md) ## Advanced Modes These modes require a custom component to be [installed](https://homeassistant-repl.rhizomatics.org.uk/configuration/server_install/index.md) on the target Home Assistant server via HACS, or use the supplied scripts to install on a local devcontainer. It adds: - Full access to the core Home Assistant Python API via `hass` - Read/write access to the actual objects, e.g. entities and their helpers - Agent friendly non-interactive mode using [Exec Mode](https://homeassistant-repl.rhizomatics.org.uk/modes/exec_mode/index.md) - A frisson of danger ## Future Developments See the [Roadmap](https://homeassistant-repl.rhizomatics.org.uk/developer/design/roadmap/index.md) for where this might go, and your feedback welcome. Note It is not intended to ever be a replacement for a Python debugger, although it may complement one. It also does not intend to replicate [PyScript](https://pyscript.net), instead focusing on standard python (PyScript uses MicroPython) even at expense of general usability or home assistance access, and not a general automation script execution service. For most non-developer cases, [homeassistant-cli](https://pypi.org/project/homeassistant-cli/) is a better choice, with pre-packaged access to devices, entities, services etc. ## Agent Support For coding agents, there is [Exec Mode](https://homeassistant-repl.rhizomatics.org.uk/modes/exec_mode/index.md) and a [marketplace skill](https://homeassistant-repl.rhizomatics.org.uk/modes/exec_mode/#agent-skill). ## Other Python REPLs These are all current with Python v3, there are others like dreampie that didn't make it past Python 2. ### New Default REPL Experiences - [Python Interactive Mode](https://docs.python.org/3/tutorial/appendix.html#tut-interac) - Core python REPL, greatly enhanced in v3.13 with PyPi code, and now the default Python terminal experience. - [VSCode Python REPL](https://code.visualstudio.com/docs/python/run#_native-repl) - Notebook style with Intellisense, bundled with the standard Microsoft VSCode Python bundle ### Classics - [Terminal iPython](https://ipython.org) - Progenitor of Jupyter notebooks, rich formatting, syntax help, shell integration etc - [ptpython](https://github.com/prompt-toolkit/ptpython) - Syntax help and formatting, mouse support and more. Part of the [prompt-toolkit](https://python-prompt-toolkit.readthedocs.io/en/latest/) project, also used by Home Assistant REPL. - [bpython](https://bpython-interpreter.org) - Syntax help, auto-complete, improved history - [xon.sh](https://xon.sh) - python-centric shell - [IDLE](https://docs.python.org/3/library/idle.html) - the original Python Foundation enhanced shell, colourized text, multi-window etc # 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](https://homeassistant-repl.rhizomatics.org.uk/modes/live_mode/index.md), 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](https://homeassistant-repl.rhizomatics.org.uk/modes/live_mode/index.md) 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: ```bash export HASS_SERVER=http://homeassistant.local:8123 export HASS_TOKEN= ``` Both can also go in a `.env` file in the directory the agent runs from, or the server can be one named in a [`config.toml`](https://homeassistant-repl.rhizomatics.org.uk/configuration/client_configuration/index.md), where a repo's `.ha-repl/config.toml` can set the default for everything run in that repo. [Plugins](https://homeassistant-repl.rhizomatics.org.uk/configuration/plugins/index.md) are run before the snippet, so helpers defined there are available to the agent; `--no-plugins` skips them. Then tell the agent the command exists, for example in its project instructions: ```markdown Use `ha-repl --json exec ''` to run Python against the development Home Assistant. `hass` is the live HomeAssistant object, `obj` the object tree, `sql("select ...")` queries the recorder. See [Exec Mode](https://homeassistant-repl.rhizomatics.org.uk/exec_mode/) documentation. ``` ### Agent Skill The repo also ships an [Agent Skill](https://agentskills.io), [`skills/ha-repl`](https://github.com/rhizomatics/homeassistant-repl/tree/main/skills/ha-repl), which teaches an agent the command, its JSON output and the rules on this page, so you don't have to write the instructions yourself. Install it in the project where you develop your component. For Claude Code, as a plugin: ```bash claude plugin marketplace add rhizomatics/agent-plugins claude plugin install homeassistant-repl@rhizomatics ``` For other agents that support skills, copy the `skills/ha-repl` directory into the agent's skills directory (for example `.claude/skills/` or `.agents/skills/`), or use the [skills](https://github.com/vercel-labs/skills) installer: ```bash npx skills add rhizomatics/homeassistant-repl ``` ### Documentation for Agents Every page of this site is also published as Markdown: add `index.md` to a page's address, or use the Markdown button at the top of the page. [`llms.txt`](https://homeassistant-repl.rhizomatics.org.uk/llms.txt) lists the pages with a line on each, and [`llms-full.txt`](https://homeassistant-repl.rhizomatics.org.uk/llms-full.txt) is all of them in one file. ## 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. ```bash 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: ```bash ha-repl --json exec 'sql("select * from states_meta", max_rows=2)' ``` ```json { "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 follows the same rule when it is plain data, so `hass.states.get("sun.sun").state` gives `"below_horizon"`. Anything else - a state object, or a list containing one - is a string holding its `repr()`, so shape the answer into plain data inside the snippet. ## 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](https://homeassistant-repl.rhizomatics.org.uk/modes/live_mode/#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. - A variable assigned by a statement that used `hass` or `obj` comes back, and is local afterwards, if it holds plain data. Anything else stays inside Home Assistant, and later statements that use it run there too. - Don't use `sql` or `hass_api` in the same statement as `hass` or `obj` - it is refused. Assign one part on its own line first. [Mixing Both Sides](https://homeassistant-repl.rhizomatics.org.uk/modes/live_mode/#mixing-both-sides) has the details and worked examples. ## 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. ```bash 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: ```json { "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 \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)` prints a summary of an object's methods and properties, which is often quicker than reading the source. `help(thing, full=True)` gives Python's own full help page instead. # Reference # Live Mode Live mode is the most powerful, flexible and dangerous way to play around with Home Assistant. Use it with a devcontainer Home Assistant or development instance unless you really know what you're doing, and even then ... ## Pre-requisites See [Live Mode Server Install](https://homeassistant-repl.rhizomatics.org.uk/configuration/server_install/index.md) ## Using the Live REPL Run `ha-repl` with the `live` argument The quickest way to run the shell is using *uv*, which you can do without cloning this repo or making any other downloads. ```bash uv run --with homeassistant-repl ha-repl live ``` Once you're in, `hass` gives you direct access to the running instance of the [HomeAssistant](https://developers.home-assistant.io/docs/dev_101_hass) class, from which everything else is accessible, its the ultimate god object for the platform. Also, `sql` gives you query access to the primary HomeAssistant database. Get help on the arguments in the usual way. `hass` and `sql` can be used together in a limited way, a statement at a time - see [Mixing Both Sides](#mixing-both-sides). ## What Runs Where The live shell is an ordinary Python session on your own machine. Import whatever you have installed, define functions, keep dataframes around - none of that touches Home Assistant. | Name | Where it lives | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `sql` | Local. Sends the query to Home Assistant, downloads the result as Arrow data, and gives you a local result object - see [SQL Access](https://homeassistant-repl.rhizomatics.org.uk/sql/index.md) | | `hass_api` | Local. A REST API client | | `hass`, `obj` | Inside Home Assistant | Since `hass` and `obj` only exist inside Home Assistant, any statement that uses one of them is sent there and run there, with its result sent back to be displayed. A variable assigned by such a statement stays there too, and later statements that use it follow it: ```python >>> import polars as pl # local >>> df = sql("select * from states").to_polars() # local, on downloaded data >>> s = hass.states.get("sun.sun") # runs inside Home Assistant >>> s.state # so does this - `s` lives there 'below_horizon' ``` The decision is made per statement, so several lines pasted or run together can use both sides. ## Mixing Both Sides The two sides are separate Python sessions, and the shell does a small amount of work to let a statement on one side use a value from the other. Treat it as a convenience for simple cases rather than something to build on: it is deliberately limited, and anything it can't do is refused rather than attempted. What it does: - **Only plain data crosses** - strings, numbers, `None`, and lists, tuples, sets and dicts of them, up to about 100,000 characters. - **Local to Home Assistant** - a local variable holding plain data is copied over before a statement that uses it. A module you imported locally is imported there under the same name. A `sql` result crosses as its rows, a list of lists. - **Home Assistant to local** - a variable assigned inside Home Assistant comes back, and is local from then on, when what it holds is plain data. So does what `obj.find_names()` and `obj.find_paths()` return: an iterator over strings, good for going through once. - **Everything else stays where it is** - states, entities, dataframes, functions and classes don't cross in either direction. - **Annotations are ignored** - a name used only in a type annotation doesn't decide where a statement runs, and is never copied over. - **One statement, one side** - `sql` and `hass_api` are local only, `hass` and `obj` are Home Assistant only, so a single statement can't use both. Assign one part to a variable first. A value that crosses is a copy. Changing it on one side doesn't change it on the other. ### Examples #### Entity names from `obj`, used locally What `find_names()` and `find_paths()` return comes back, so the next statement can go through it with anything local - here, the REST client. ```python names = obj.find_names(domain="light") {n: hass_api.get_state(entity_id=n).state for n in names} ``` Runs inside Home Assistant, then locally. #### Tree paths from `obj`, filtered locally Once the paths are back, going through them is ordinary local Python. ```python paths = obj.find_paths("/demo") sorted(p for p in paths if "kitchen" in p) ``` Runs inside Home Assistant, then locally. #### A local list, used inside Home Assistant Plain local data is copied over before the statement that needs it. ```python wanted = ["light.kitchen", "sun.sun"] {n: hass.states.get(n).state for n in wanted} ``` Runs locally, then inside Home Assistant. #### A number worked out inside Home Assistant Numbers and strings come back just as lists of them do. ```python count = len(hass.states.async_all()) count * 2 ``` Runs inside Home Assistant, then locally. #### Rows from `sql`, looked up in `hass` A `sql` result is local, and crosses as its rows - a list of lists - when a statement inside Home Assistant loops over it. ```python r = sql("select entity_id from states_meta") {row[0]: hass.states.get(row[0]).state for row in r} ``` Runs locally, then inside Home Assistant. #### Objects stay inside Home Assistant A state, an entity, or anything else that isn't plain data stays where it is, and later statements that use it run there too. ```python hass.states.get("sun.sun").state ``` Runs inside Home Assistant. #### Entity names from `obj`, found in one statement and used in `hass` in another The names come back as local data, and are copied over again for the statement that needs them inside Home Assistant. ```python names = obj.find_names(domain="light") {n: hass.states.get(n).state for n in names} ``` Runs inside Home Assistant, then inside Home Assistant. ### Not Supported The shell refuses these with an explanation, rather than running them. #### Both sides in one statement `hass_api` and `sql` only exist locally, `hass` and `obj` only inside Home Assistant, and a statement runs in one place. Split it in two, as in the first example. ```python {n: hass_api.get_state(entity_id=n).state for n in obj.find_names()} ``` #### A local function, called on something from `hass` Only data crosses - functions, classes and dataframes don't. ```python def shout(text): return text.upper() shout(hass.states.get("sun.sun").state) ``` ## One-Shot Snippets and Agents `ha-repl exec` runs a snippet exactly as the live shell would, then exits - see [Exec Mode](https://homeassistant-repl.rhizomatics.org.uk/modes/exec_mode/index.md), which covers its JSON output and use by coding agents. ## Running from a clone/fork of this repo If you do have this repo checked out, you can also use a direct `run` which means you can also tinker locally with `homeassistant_repl` code. ```bash uv run ha-repl ``` ### Install Local Home Assistant with the Live Server The `homeassistant-repl` repo has a [DevContainer](https://containers.dev) defined, and helper scripts in the `/dev` directory to manage it. Dev instance (devcontainer, or directly on a host with Python 3.14): ```sh dev/setup.sh # installs HA into its own venv + the CLI (devcontainer runs this) dev/restart-ha.sh # Kill current HA instance and start a new dev/run-ha.sh # HA on :8123 with custom_components/ha_repl_server symlinked in uv run python dev/bootstrap.py # onboard (user dev/dev) and write dev/.env with a token ``` Using it (host or container): ```sh set -a; . dev/.env; set +a uv run ha-repl # interactive uv run ha-repl exec 'hass.states.get("sun.sun")' uv run ha-repl exec - <<'PY' await hass.services.async_call("light", "toggle", {"entity_id": "light.kitchen_lights"}, blocking=True) hass.states.get("light.kitchen_lights").state PY ``` # The Object Tree All of the objects (only entities for now) are arranged in a giant tree, like a file system, exposed as the global variable `obj` and implemented as Python [Mapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping) object (which provides the [MappingView](https://docs.python.org/3/library/collections.abc.html#collections.abc.MappingView) views [ItemsView](https://docs.python.org/3/library/collections.abc.html#collections.abc.ItemsView) and [KeysView](https://docs.python.org/3/library/collections.abc.html#collections.abc.KeysView)) `obj` offers: - Dictionary style access, using `[]` - Raises `KeyError` if entity or sub-path doesn't exist - Each level of the tree returns a sub-tree - `keys()`,`values()`,`items()` of the sub-tree shows only that level - An `OrderedView` is used rather than plain `MappingView` so can be accessed like a `list` and items are alphabetically organized - `find()` - Returns a flat iterable of the entire tree - Optionally restrict by `platform`,`area`,`label` - `show()` - Dump the most useful info on an object to console `help(obj)` gives a summary of all of this at the prompt. All the usual Python tricks can of course also be used, iterators, comprehensions, classes, lambdas or a simple `len()`. ### `obj[]` This allows dictionary ('Mapping') access to the object tree. It can also accept a simple entity name and bypass the tree structure altogether. In the example tree below, the objects and subtrees of objects can be accessed like: ```python >>> obj["/rflink/sensor/shed_temperature"].state # prints out temperature >>> obj[ "/rflink/sensor/shed_temperature" ].state_attributes # prints out additional attributes >>> obj["/rflink"] # the 'light','binary_sensor' and 'sensor' subtrees for rflink >>> obj["/rflink/sensor"] # all the sensors for rflink >>> len(obj["/rflink/sensor"]) # count of rflink sensors in this example >>> obj["sensor.shed_temperature"] # non-path simple entity name mode ``` ##### Example Tree ```text ... - mqtt - rflink - light - staircase_ceiling - shed - switch - upstairs_pixie - binary_sensor - porch_pir - shed_door - sensor - shed_temperature - kitchen_humidity ... ``` Note In the roadmap, there will be a visual Object Browser to view and select entities. For now, it is accessible only via Python code. It also may extend beyond entities, to things like areas, users, categories and devices. #### `obj.find('..')` Where the dictionary access gives a nested directory view of the object tree, `find` provides a flat iteration with no order guarantees, so its fast and simple and can be sorted the usual Python way if needed. `find` functions can accept a full or partial path, or a regular expression. `find` also has built in filters, to narrow the big list of objects by one or more `platform`,`domain`,`area`,`label` - each of these will take a single string or list of strings, and they can be combined to narrow down the list. ```python >>> {(o.entity_id, o.state) for o in obj.find(domain="binary_sensor")} ``` Or using a regular expression: ```python >>> {(o.entity_id, o.state) for o in obj.find("/rflink/binary_sensor/*._leak")} ``` ##### Raw Objects In API Client mode, `find()` returns a local proxy for the remote class, normalized to look more like the same object you'd get in live mode. Switching `raw=True` will bypass this and you'll get the object untouched as it was received from the API. #### `obj.find_paths(..)` Identical to `obj.find()` except it only returns an iterable of the object paths rather than the objects themselves. Ideal for plugging into some logic that will then call `obj[path]` on each one. ```python >>> list(obj.find(area="kitchen")) # list names of all entities in kitchen >>> sorted(obj.find(area=["kitchen", "shed"])) >>> list(obj.find(".*_leak")) ``` #### `obj.find_names(..)` Same as `obj.find_paths()` except it returns the object bare name, as it would appear in Home Assistant, e.g. `sensor.bathroom_humidity` #### `obj.show(..)` The `show` function will give a pretty version of an object where it knows how. For entities this means it combines the `RegistryEntry` and `Entity` information, drops some boring internal stuff, filters out all the `None` values and turns `datetime.datetime` structures into local date times. ```yaml obj.show('/unifi/sensor/kitchen_wifi_cpu_utilization') ``` # SQL Access Home Assistant has an internal SQL database front-ended by the [Recorder](https://www.home-assistant.io/integrations/recorder/) integration. It holds, history, activity, long term statistics and more. Underneath it can by Sqlite (most common), MariaDB, MySQL or PostgreSQL. When in `live` mode, the `ha-repl` exposes an `sql` variable, which allows access to this database, via the Recorder interface. `sql` and its results are local objects. A query is sent to Home Assistant, the rows come back as one download, and everything after that - `show()`, slicing, dataframes, CSV export - works on that local copy, with whatever libraries you have installed locally. Nothing extra needs installing in Home Assistant. Info Data is serialized as Apache Arrow using the `nanofeather` library - this provides column-oriented data for good compression, zero-copy reuse into dataframes and good compatibility with popular data libraries like `polars` and `pandas`. ### `sql` Object | Operation | Description | | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `sql("")` | Send a query to the database and wait for results | | `sql("", legacy=True)` | The same, with [legacy columns](#legacy-columns) in view | | `sql.max_rows` | Read/write property to set the default row limit for results in this session | | `sql.table("")` | Returns the `Table` object that describes the named table and its contents, or raises `KeyError` if there is no such table | | `sql.table("", legacy=True)` | The same, with the table's [legacy columns](#legacy-columns) included | | `sql.tables` | Returns a list of `Table` objects that describe each table and its contents | For example this will show the size of the `statistics` table. ```python >>> results=sql('select * from event_types') >>> sql('select count(*) from statistics').show() ``` ### Result Objects On the result object, `show()` will display data in tabular form at the command, using the `rich` library's table support. The result will have its rows automatically limited, so it doesn't lock up the console - use `max_rows` to override this. The columns shown are the same ones a row of the result has, so `show()` and `[0]` always agree; use `max_cols` to cap how many, or `columns` to set the list of columns specifically. A single row can be picked out by position, so `[0]` for the first row or `[-1]` for the last - see [Row Objects](#row-objects). The results can also be sliced like a Python list, so `[:10]` for the first 10 rows or `[-10:]` for the last 10. Slices share the downloaded data rather than copying it. For anything more advanced - a step, filtering, sorting, joins - turn the result into a dataframe with `to_polars()`, which also uses the downloaded Arrow data directly. You can also call `len()` on the results to get the number of rows. | Operation. | Description | | -------------- | ------------------------------------------------------------------------------------------------------------------------- | | `rowcount` | Size of result set, may not be size of table due to row limits | | `truncated` | `True` if results were truncated due to row limits | | `legacy` | Read/write flag, set to `True` to bring [legacy columns](#legacy-columns) into view | | `table` | The `Table` object describing this table, or `None` if the columns don't identify a single table | | `column_names` | List of the column names in the table, in order | | `show()` | Dump the table data to the console | | `project()` | Create a new result object with a limited set of columns based on this one | | `sample()` | Randomly sample selected quantity of rows out of the result set | | `export_csv()` | Write the results to a local CSV file, by default named after the table (`result.csv` if there isn't one) | | `arrow()` | The result as an Arrow array, for Arrow-aware libraries, e.g. `polars.DataFrame(r.arrow())` or `pyarrow.table(r.arrow())` | | `arrow_ipc()` | The result as Arrow IPC stream bytes, e.g. to save to a file | | `to_json()` | The results as a JSON string of `columns`, `rows`, `rowcount` and `truncated` | | `to_dicts()` | Extract a `list` of `dict` objects from the results | | `to_pandas()` | Turn the results into a *pandas* dataframe, if pandas installed | | `to_polars()` | Turn the results into a *polars* dataframe, if polars installed | ### Row Objects Indexing a result with a row number gives a `Row` object. It reads like a list of the row's values, in column order, and can also be indexed by column name. | Operation | Description | | -------------- | ---------------------------------------------------------------------------- | | `values` | List of the values in the row, in column order | | `column_names` | List of the column names, in the same order | | `table` | The `Table` object of the result the row came from, or `None` if it had none | | `to_dict()` | The row as a `dict` of column name to value | ```python >>> sql("select count(*) from events")[0][0] >>> row = sql("select * from states_meta")[0] >>> row["entity_id"] >>> {col: sql(f"select count(distinct {col}) from events")[0][0] for col in sql.table("events").column_names} ``` ### Table Objects A `Table` object describes one table, using Home Assistant's own definition of it in `homeassistant.components.recorder.db_schema`. | Operation | Description | | -------------- | ------------------------------------------------------------------------------------------------ | | `name` | The table's name in the database, e.g. `states` | | `class_name` | The name of the class the Recorder uses for this table, e.g. `States`, to look up in `db_schema` | | `description` | That class's docstring, e.g. `State change history.` | | `columns` | List of `Column` objects, each with a `name`, `type` and `legacy` flag | | `column_names` | List of the column names, in order | Both leave out the [legacy columns](#legacy-columns), unless the table came from `sql.table("", legacy=True)`. ### Legacy Columns Some Recorder tables still have columns that Home Assistant no longer writes to, because the data moved elsewhere - `entity_id` in `states`, for example, is now in `states_meta`. Home Assistant marks these in `db_schema` as `UNUSED_LEGACY_COLUMN`, and they are empty for anything recorded recently. They are kept out of view by default, both in a `Table` and in a query's result. A query still downloads them, but the result leaves them out of everything it does - `show()`, rows, `column_names`, dataframes, exports - so `select * from states` shows only the columns in use. A legacy column is in view when: - the query names it - the query is made with `legacy=True` - `legacy` is set to `True` on the result afterwards, which needs no new query ```python >>> r = sql("select * from states") # columns in use >>> r.legacy = True # now every column >>> sql("select * from states", legacy=True) # every column from the start >>> sql("select state_id, entity_id from states") # named, so in view >>> sql.table("states").column_names # columns in use >>> sql.table("states", legacy=True).column_names # every column ``` A query is checked by its words rather than parsed, so a legacy column mentioned anywhere in it, such as in a `where`, is in view. ### More Help Use `help(sql)`, or call `help(..)` on a result, row or table object, to get reminded of the API. It gives a short summary of the methods, properties and attributes; add `full=True` for Python's own full help page. # Alternative Integration Other ways of using Home Assistant REPL without using the supplied shell. ## Using `homeassistant_repl` from Plain Python The `api` mode REPL above is just a thin wrapper around `homeassistant_repl.connect()` - the same call works from a one-off script, a notebook, or an interactive `python -m asyncio` session (the standard library's REPL that supports top-level `await`, unlike plain `python`), with no dependency on the `ha-repl` executable at all. Install the package first (`pip install homeassistant-repl`, or `uv add homeassistant-repl` in a project) - the importable package is named `homeassistant_repl`. ```bash $ python -m asyncio >>> import homeassistant_repl >>> obj = await homeassistant_repl.connect() # reads $HASS_SERVER/$HASS_TOKEN, same as the CLI >>> obj["/rflink/binary_sensor/hall_pir"].state 'off' >>> [e.entity_id for e in obj.find(domain="light")] ['light.kitchen_lights', 'light.shed'] ``` Or pass the URL/token explicitly rather than via environment variables, and use it in a script with `asyncio.run`: ```python import asyncio import homeassistant_repl async def main() -> None: obj = await homeassistant_repl.connect( url="http://homeassistant.local:8123", token="..." ) print(obj["/rflink/binary_sensor/hall_pir"].state) asyncio.run(main()) ``` `obj` behaves identically to the one bound in the REPL - everything under The Object Tree below applies. `connect()` only gets you `api` mode (read-only, no `hass`); `live` mode's live object tree requires the `ha_repl_server` HACS component and talks to it over the same underlying `Client`, but isn't exposed as a standalone importable helper. `connect()` reads the same [`config.toml`](https://homeassistant-repl.rhizomatics.org.uk/configuration/client_configuration/index.md) as the CLI, so a server can be given by name - `await homeassistant_repl.connect("house")` - and the `default` server is used when no argument and no `HASS_SERVER` is given. Plugins belong to the shells, and are not run by `connect()`. By default the cached snapshot is reused for 30 seconds (`ttl=` to change that) and the websocket connection is left open for the life of the process; call `await obj.cache.refresh()` for a fresh snapshot on demand, or `await obj.cache.client.close()` when you're done with it if that matters for your script. ## iPython Example ```bash $ ipython >>> import homeassistant_repl >>> obj = await homeassistant_repl.connect() ``` ## Marimo Example ## Web Based Terminal [ttyd](https://tsl0922.github.io/ttyd/) is available easily via Mise, Homebrew or `opkg`. The http port will be logged at startup. It can be a dangerous security hole if not carefully used. ```bash ttyd uv run --with homeassistant-repl ha-repl api ``` Recent versions of ttyd are read-only unless started with `--writable`, and have no login unless given `--credential user:password`. Anyone who can reach the port can use the shell, and in `live` mode that means running any Python inside Home Assistant. ### In a Docker Container This example image serves the live shell on port 7681, for where installing ttyd and `uv` directly isn't convenient. It hasn't been widely tested, so treat it as a starting point. ```dockerfile FROM ghcr.io/astral-sh/uv:python3.14-bookworm-slim RUN apt-get update \ && apt-get install -y --no-install-recommends ttyd \ && rm -rf /var/lib/apt/lists/* # installs the ha-repl command into /usr/local/bin ENV UV_TOOL_BIN_DIR=/usr/local/bin RUN uv tool install homeassistant-repl EXPOSE 7681 CMD ["ttyd", "--writable", "--port", "7681", "ha-repl", "live"] ``` Build it, then run it with the address and token of the Home Assistant to connect to, published only on the local machine: ```bash docker build -t ha-repl-web . docker run --rm -p 127.0.0.1:7681:7681 \ -e HASS_SERVER=http://homeassistant.local:8123 \ -e HASS_TOKEN= \ ha-repl-web ``` The shell is then at . To set a login, or run `api` mode instead, give the whole command at the end of `docker run`, for example `ttyd --writable --credential me:secret ha-repl live`. ## From a Home Assistant Terminal Add-on On Home Assistant OS there is no `uv`, or recent enough Python, on the host - but a terminal add-on runs in its own container, where both can be added. This gives a shell in the browser, from the Home Assistant sidebar, with nothing to install on another machine. With the [Advanced SSH & Web Terminal](https://github.com/hassio-addons/addon-ssh) add-on, add `uv` to the packages it installs each time it starts, in the add-on's configuration: ```yaml packages: - uv ``` Restart the add-on, open its terminal, and run the shell - `uv` downloads a suitable Python of its own: ```bash uvx --from homeassistant-repl ha-repl live ``` No `HASS_SERVER` or `HASS_TOKEN` is needed if the add-on provides a `SUPERVISOR_TOKEN`, since `ha-repl` then connects through the Supervisor. Otherwise set both, as for any other machine. `uv` keeps its downloads in the home directory, which the add-on may not keep between restarts. To avoid downloading Python and the packages again each time, point `UV_CACHE_DIR` and `UV_PYTHON_INSTALL_DIR` at a directory that is kept, such as one under `/config`. ### Studio Code Server The [Studio Code Server](https://github.com/hassio-addons/addon-vscode) add-on also works, from its built-in terminal, with two differences. It is based on Debian, which has no `uv` package to add to the configuration, so install `uv` with its own installer: ```bash curl -LsSf https://astral.sh/uv/install.sh | sh ``` Its Python is also older than `ha-repl` needs, so ask `uv` for a newer one when running the shell: ```bash uv run --python 3.14 --with homeassistant-repl ha-repl live ``` To have `uv` there each time the add-on starts, put the install command in the add-on's `init_commands` configuration. # Optional 1. Safe-ish by default, full power for those who can handle it 1. No pickling unless unavoidable - SQL results are sent back to client as `feather` Apache Arrow dataframes - Home Assistant API calls as REST API and interpreted on client - `hass` needs client side objects serialized when arguments, and server-side responses 1. Maximum freedom for the developer on the client, maximum restrictions on the server side - No limits on memory, CPU, package loading, (within reason) Python versions on the client side REPL. - Server side code is all regrettable, every dependency brings some extra risk to the running instance 1. Agents are first-class users. They can benefit from real world feedback of code and ideas, so live mode is likely to be the most useful, and also the most dangerous unless restricted to devcontainer type environments. - In commercial environments, cloning - ideally CoW or zero-copy style - of production environments to development ones is a common way to get realistic or even repro environments accessible safely.