227 lines
11 KiB
Markdown
227 lines
11 KiB
Markdown
---
|
||
name: homeassistant-integration
|
||
description: "Connect Home Assistant to Hermes — token setup, toolset enablement, and smart home control via Hermes tools."
|
||
version: 1.2.0
|
||
author: ray
|
||
platforms: [linux]
|
||
created_by: "agent"
|
||
metadata:
|
||
hermes:
|
||
tags: [smart-home, homeassistant, integration, ha]
|
||
requires:
|
||
env_vars: ["HASS_TOKEN", "HASS_URL"]
|
||
toolsets: ["homeassistant"]
|
||
gateway_restart: true
|
||
---
|
||
|
||
# Home Assistant + Hermes Integration
|
||
|
||
Connect a Home Assistant instance to Hermes so the agent can list entities, read sensor states, discover services, and control devices (lights, switches, climate, media players, etc.) via the `homeassistant` toolset.
|
||
|
||
## Prerequisites
|
||
|
||
- A running Home Assistant instance (local network or remote)
|
||
- A **Long-Lived Access Token** from your HA profile page (click your username → Security → Long-Lived Access Tokens)
|
||
- The HA server accessible from the Hermes host
|
||
|
||
## Setup
|
||
|
||
### 1. Get a Long-Lived Access Token
|
||
|
||
In Home Assistant: **click your user profile** (bottom-left) → **Security** → **Long-Lived Access Tokens** → **Create Token**.
|
||
|
||
The token is a JWT that looks like:
|
||
```
|
||
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOi...<rest of token>
|
||
```
|
||
|
||
You can decode it to verify:
|
||
```bash
|
||
python3 -c "
|
||
import base64, json
|
||
payload = base64.urlsafe_b64decode('PAYLOAD_SECTION' + '==')
|
||
data = json.loads(payload)
|
||
print(f'Issuer: {data[\"iss\"]}')
|
||
print(f'Expires: {data[\"exp\"]}')
|
||
"
|
||
```
|
||
HA long-lived access tokens typically expire in 10 years.
|
||
|
||
### 2. Set the Environment Variables
|
||
|
||
```bash
|
||
hermes config set HASS_URL http://YOUR_HA_HOST:8123
|
||
hermes config set HASS_TOKEN <your-long-lived-access-token>
|
||
```
|
||
|
||
`hermes config set` auto-detects secrets — the URL goes to `config.yaml`, the token goes to `.env`. The tool code reads **both** from environment variables, so if `HASS_URL` landed in config.yaml instead of `.env`, add it to `.env` as well:
|
||
|
||
```bash
|
||
echo 'HASS_URL=http://YOUR_HA_HOST:8123' >> ~/.hermes/.env
|
||
```
|
||
|
||
Verify they're in place:
|
||
```bash
|
||
grep HASS ~/.hermes/.env
|
||
```
|
||
|
||
### 3. Enable the Toolset
|
||
|
||
```bash
|
||
hermes tools enable homeassistant
|
||
```
|
||
|
||
Verify:
|
||
```bash
|
||
hermes tools list | grep homeassistant
|
||
# ✓ enabled homeassistant 🏠 Home Assistant
|
||
```
|
||
|
||
### 4. Verify the Token Works
|
||
|
||
Test with a direct API call before restarting the gateway:
|
||
|
||
```bash
|
||
python3 -c "
|
||
import json, urllib.request, subprocess
|
||
token = subprocess.run(['grep', 'HASS_TOKEN', '$HOME/.hermes/.env'],
|
||
capture_output=True, text=True).stdout.strip().split('=',1)[1]
|
||
req = urllib.request.Request('http://YOUR_HA_HOST:8123/api/',
|
||
headers={'Authorization': f'Bearer {token}'})
|
||
data = json.loads(urllib.request.urlopen(req, timeout=10).read())
|
||
print(f'HA: {data.get(\"location_name\", \"unnamed\")}')
|
||
"
|
||
```
|
||
|
||
Also check entity count:
|
||
```bash
|
||
# Same pattern, hit /api/states instead and count by domain
|
||
```
|
||
|
||
If you get **HTTP 200**, the token is valid and the URL is reachable.
|
||
|
||
### 5. Restart the Gateway
|
||
|
||
Tool changes require a new session. Restart the gateway:
|
||
|
||
```bash
|
||
hermes gateway restart
|
||
```
|
||
|
||
After restart, the agent has access to four HA tools:
|
||
|
||
| Tool | Purpose |
|
||
|------|---------|
|
||
| `ha_list_entities` | List/filter entities by domain or area |
|
||
| `ha_get_state` | Get detailed state + attributes of one entity |
|
||
| `ha_list_services` | Discover available services (actions) per domain |
|
||
| `ha_call_service` | Control a device (turn_on, turn_off, set_temperature, etc.) |
|
||
|
||
## Security
|
||
|
||
The `homeassistant_tool.py` implementation blocks these high-risk service domains for safety:
|
||
`shell_command`, `command_line`, `python_script`, `pyscript`, `hassio`, `rest_command`
|
||
|
||
## Automation Patterns
|
||
|
||
### Cron-Based Condition Monitoring
|
||
|
||
Use Hermes cron jobs to poll an HA entity and act when a condition is met (e.g., turn off AC when temp drops to target). The cron job should self-terminate after acting.
|
||
|
||
**Recipe — trigger action on sensor threshold:**
|
||
|
||
```
|
||
cronjob(action='create',
|
||
name='<descriptive name>',
|
||
prompt='Check <entity_id> using ha_get_state. If <condition>, call ha_call_service to <action> and report success, then use cronjob to remove this monitoring job (job_id in context). If condition not met, report current state briefly and do nothing.',
|
||
schedule='*/10 22-23,0-2 * * *', # every 10 min from 10pm–2am (use CRON EXPRESSION, not "10m" — that's a one-shot)
|
||
repeat=20,
|
||
toolsets=['homeassistant', 'cronjob'])
|
||
```
|
||
|
||
Key details:
|
||
- **toolsets**: Must include both `homeassistant` and `cronjob` so the cron agent can check sensors, call services, and remove itself.
|
||
- **Schedule**: Use `*/N hour-range * * *` for time-windowed polling (e.g., `*/7 22-23,0-2 * * *` = every 7 minutes from 10 PM through 2 AM). Avoid starting monitoring before the user wants it — use the cron expression to delay first run.
|
||
- **CRITICAL — schedule format**: Simple time strings like `"10m"` or `"30m"` are parsed as **one-shot** ("once in 10 minutes"), NOT recurring. The cron will run exactly once and then stop. Always use cron expressions (`*/10 * * * *`) or `"every 10m"` for recurring jobs. A job that silently runs once and completes without acting is the #1 failure mode for HVAC monitoring.
|
||
- **Repeat limit**: Cap with `repeat` to prevent indefinite polling. With N-minute intervals, `repeat=20` covers ~N×20 minutes.
|
||
- **Prompt must include self-removal**: The job should call `cronjob(action='remove', job_id=...)` after acting so it doesn't keep polling indefinitely.
|
||
- **Schedule corrections**: Users often tweak poll intervals or start times after creation. Use `cronjob(action='update', job_id=..., schedule=...)` rather than deleting and recreating.
|
||
|
||
## Pitfalls
|
||
|
||
- **Home Assistant behind VPS reverse proxy:** When proxying HA through an external nginx (VPS + Tailscale), add the proxy's Tailscale IP to `trusted_proxies` in `configuration.yaml` or HA returns 400:
|
||
```yaml
|
||
http:
|
||
use_x_forwarded_for: true
|
||
trusted_proxies:
|
||
- 127.0.0.1
|
||
- 100.86.68.23 # VPS Tailscale IP
|
||
```
|
||
Restart: `docker restart homeassistant`. See `docker-service-deployment` skill, `references/vps-migration.md` for the full VPS setup pattern.
|
||
- **CRITICAL — Cron schedule format:** Bare duration strings like `"10m"` or `"30m"` are ONE-SHOT ("once in X minutes"). The job runs exactly once and silently completes — the #1 failure mode for HVAC monitoring. Always use **cron expressions**: `"*/10 * * * *"` (every 10 minutes), `"*/7 22-23,0-2 * * *"` (time-windowed). Alternative: `"every 10m"` shorthand. Verify with `cronjob(action='list')` after creation — `"once in Nm"` means it won't repeat.
|
||
- **Climate vs sensor entities:** Climate entities (`climate.upstairs`) are the thermostat — call `ha_call_service` to control them. Sensor entities (`sensor.upstairs_temperature`) are read-only readings. For threshold monitoring, check the sensor for the reading but control the climate entity.
|
||
- **HASS_URL must be in .env**: The tool reads `os.getenv("HASS_URL")`, not `config.yaml`. Add it to `.env` explicitly.
|
||
- **Docs 404**: The Hermes docs page for Home Assistant integration returns 404. Canonical reference: `tools/homeassistant_tool.py` and `gateway/config.py`.
|
||
- **Subprocesses don't inherit .env**: When testing with `python3 -c`, read secrets via `subprocess.run(['grep', ...])` instead of `os.getenv()`.
|
||
- **Gateway restart required**: Toolset changes need `hermes gateway restart`.
|
||
- **Internal vs proxy URL**: The tool needs the direct API URL (e.g., `http://192.168.50.98:8123`), not the external proxy port.
|
||
- **Entity registry may 404**: Use `/api/config/config_entries/entry` instead to discover integrations and device metadata.
|
||
|
||
### Two-Phase Monitoring (Wide → Narrow Polling)
|
||
|
||
When the target is far away, poll infrequently to save resources. When it nears the threshold, switch to tight polling automatically.
|
||
|
||
**How it works:** The cron job starts with a wide interval (e.g., every 30 min). When the sensor value enters a "close" zone, the job updates its own schedule to a tight interval (e.g., every 7 min). When the final threshold is hit, it acts and removes itself.
|
||
|
||
**Recipe:**
|
||
|
||
```
|
||
# Phase 1 — wide polling (every 30 min on the half-hour, 10pm–2am)
|
||
cronjob(action='create',
|
||
name='Turn off AC when temp hits 78°F',
|
||
prompt='Check climate.upstairs with ha_get_state. '
|
||
'If temp > 79°F: report briefly, do nothing (stay in wide-poll mode). '
|
||
'If temp ≤ 79°F and > 78°F: UPGRADE this job to tight polling — '
|
||
'cronjob(action=update, job_id=THIS_JOB_ID, schedule="*/7 22-23,0-2 * * *"). '\
|
||
'Report "now monitoring closely." '
|
||
'If temp ≤ 78°F: ha_call_service(domain=climate, service=turn_off, '
|
||
'entity_id=climate.upstairs), report success, remove this job.',
|
||
schedule='0,30 22-23,0-2 * * *',
|
||
repeat=20,
|
||
toolsets=['homeassistant','cronjob'])
|
||
```
|
||
|
||
Once upgraded to Phase 2, the job's prompt should know it's in tight mode: check temp, act if ≤ target, otherwise report and wait. The prompt must handle both modes so it works correctly after a self-upgrade.
|
||
|
||
Key points:
|
||
- **Phase 1 prompt** must include the self-upgrade logic (wide → tight) AND the final action logic (act + self-remove).
|
||
- **Phase 2 prompt** (after upgrade) only needs: check, act-if-condition-met, otherwise report.
|
||
- **Use `repeat`** to cap total runs so the job doesn't poll forever if the condition never triggers.
|
||
- Users often specify multiple constraints (interval, start time, threshold-based frequency). Ask clarifying questions if the intent is ambiguous, and prefer the two-phase pattern when the target is expected to drift slowly.
|
||
|
||
## IoT Device Onboarding
|
||
|
||
For finding, identifying, and adding new WiFi IoT devices to the network and Home Assistant — WiFi setup, IP discovery, MAC vendor lookup, LAN mode enablement, and HA integration — see `references/iot-device-onboarding.md`.
|
||
|
||
## Identifying Devices & Integrations
|
||
|
||
### Thermostat Model / Integration Discovery
|
||
|
||
The entity states API (`/api/states`) doesn't expose manufacturer or model info. To find what integration provides a climate entity and what hardware it's connected to, query the HA config entries API:
|
||
|
||
```python
|
||
import json, urllib.request
|
||
|
||
# List all config entries (integrations)
|
||
req = urllib.request.Request(
|
||
'http://HA_HOST:8123/api/config/config_entries/entry',
|
||
headers={'Authorization': f'Bearer {token}'})
|
||
entries = json.loads(urllib.request.urlopen(req).read())
|
||
|
||
for entry in entries:
|
||
if entry.get('domain') == 'honeywell': # or 'ecobee', 'nest', etc.
|
||
print(f"Title: {entry['title']}, State: {entry['state']}")
|
||
```
|
||
|
||
The `domain` field reveals the integration (e.g., `honeywell`, `ecobee`, `nest`). The entity registry endpoints (`/api/config/entity_registry/*`) may 404 depending on HA version or reverse-proxy setup — the config entries API is a reliable fallback.
|