hass-history-mcp
by carlosperezc
README.md
# hass-history-mcp
An MCP (Model Context Protocol) server that gives Claude access to **historical**
Home Assistant data — something the standard Home Assistant MCP integration
doesn't expose. It answers questions like:
- How much power has a given entity drawn over the last week?
- What's my estimated energy consumption (kWh) for a device or circuit?
- Has a device been offline recently, and for how long?
- What was the last reported state/value of an entity, and how long ago?
It works by querying an **existing InfluxDB 1.8** instance that Home Assistant
is already writing sensor data to (via the built-in [InfluxDB integration](https://www.home-assistant.io/integrations/influxdb/)),
rather than talking to Home Assistant's recorder database directly. This means
it can see further back than HA's default recorder purge window (commonly
10 days), since InfluxDB retention is independent of that.
## How it works
Home Assistant's InfluxDB integration writes each entity's state as a point
tagged with `entity_id` and `domain`, with the numeric value in a `value` field.
The measurement name is usually the entity's unit of measurement (e.g. `W`,
`kWh`, `%`), which varies per sensor — so every query in this server matches
across measurements with a regex (`/.*/`) and filters by the `entity_id` tag
instead. This makes the tools work regardless of your exact
`default_measurement` configuration.
Two things to know about real-world databases:
- **`friendly_name` is usually a *field*, not a tag** (often `friendly_name_str`).
Only `entity_id` and `domain` are reliably tags, so you can't `GROUP BY`
friendly name unless you've added it to `tags_attributes` in your Home
Assistant InfluxDB config. Adding it applies to new data only.
- **The same entity can appear in several measurements.** Integrations that set
a different `default_measurement` write to names like
`sensor.my_entity_battery` alongside the unit-based `%`. Because InfluxQL
applies `LIMIT` *per series*, a query spanning measurements returns one row
per measurement — so "most recent" must be resolved by comparing timestamps,
not by taking the first row. `get_last_seen` and `get_device_uptime` handle
this; keep it in mind for your own `run_influxql` queries.
## Timezones
InfluxDB stores everything in UTC. Set `LOCAL_TZ` to your IANA zone (default
`America/Toronto`) so that bucketed queries align to **local** midnight rather
than UTC midnight — otherwise a "daily" energy total silently covers a window
offset by your UTC offset. Every history tool also accepts a per-call `tz`
argument that overrides `LOCAL_TZ`, and returns timestamps carrying a local
offset. Use the zone name rather than a fixed offset so daylight-saving
transitions are handled correctly.
## Tools
| Tool | Description |
|---|---|
| `list_entities(domain=None, limit=200)` | List known entity_id values, optionally filtered by domain (`sensor`, `switch`, `binary_sensor`, ...). |
| `get_entity_history(entity_id, hours=24, interval=None, tz=None)` | Raw history, or bucketed averages if `interval` (e.g. `1h`, `15m`) is given. Buckets align to local midnight. |
| `get_energy_consumption(entity_id, days=7, bucket="1d", tz=None)` | Integrates a Watts sensor into kWh, bucketed by local day/hour. |
| `get_last_seen(entity_id, tz=None)` | Most recent value across **all** measurements, in UTC and local time, plus age. |
| `get_device_uptime(entity_id, days=7, expected_interval_seconds=300, gap_multiplier=3.0, tz=None)` | Detects gaps between data points to estimate uptime % and list offline windows in local time. |
| `run_influxql(query)` | Escape hatch for custom queries — restricted to `SELECT`/`SHOW`. |
## Configuration
Set via environment variables:
| Variable | Default | Notes |
|---|---|---|
| `INFLUXDB_HOST` | `influxdb` | Container name if on the same docker network, else host IP/DNS |
| `INFLUXDB_PORT` | `8086` | |
| `INFLUXDB_DB` | `homeassistant` | Database name — note HA's default has no underscore |
| `INFLUXDB_USER` | *(empty)* | Only needed if auth is enabled |
| `INFLUXDB_PASSWORD` | *(empty)* | |
| `MCP_HOST` | `0.0.0.0` | |
| `MCP_PORT` | `8006` | |
| `LOCAL_TZ` | `America/Toronto` | IANA zone for local-time bucketing |
The compose files read these from your environment, so nothing host-specific is
committed. Set them in Portainer under **Stack → Environment variables**, or in
a `.env` file beside the compose file:
```
INFLUXDB_HOST=10.0.0.5
LOCAL_TZ=America/Toronto
```
Requires the MCP Python SDK v2 (`mcp>=2`). v1's `mcp.server.fastmcp` module was
removed in 2.x — `MCPServer` replaces `FastMCP`.
## Running with Docker
```bash
docker compose up -d --build
```
`docker-compose.yml` runs it standalone. If you'd rather add it to an existing
stack, `docker-compose.snippet.yml` holds just the service block — paste it
under that file's `services:` key.
### Portainer
Use **Stacks → Add stack → Repository** and point it at this repo, so Portainer
has the source needed to build the image. The Web editor tab can't build from a
Dockerfile — pasting the compose file there will fail unless you swap `build: .`
for a prebuilt `image:`.
Once running, point your MCP client (e.g. Claude Desktop) at:
```
http://<host>:8006/mcp
```
## Verifying your schema
Before relying on the results, confirm the InfluxDB schema matches what this
server expects. Run these in order — each is bounded and cheap:
```sql
-- 1. Measurement names (these are your units: W, kWh, %, ...)
SHOW MEASUREMENTS LIMIT 20
-- 2. Confirm the entity_id tag exists
SHOW TAG VALUES FROM /.*/ WITH KEY = "entity_id" LIMIT 10
-- 3. Confirm the field is named `value`, against ONE known measurement
SELECT * FROM "W" WHERE time > now() - 1h LIMIT 1
```
> **Do not run `SELECT * FROM /.*/ LIMIT 1`.** `LIMIT` applies *per series*, not
> globally, so that query materializes one row for every series across every
> measurement at once. On a database with a few thousand entities it can grow
> InfluxDB's memory to many gigabytes and trigger the kernel OOM killer — taking
> down InfluxDB and potentially the whole host. Always bound schema checks to a
> single measurement and a time range, as in step 3 above.
If your `tags_attributes` / `default_measurement` Home Assistant config is
customized, the field name might not be `value` — in that case, adjust the
queries in `server.py` accordingly.
## License
MIT — use however you like.This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues