Files
hermes-config/skills/smart-home/homeassistant-integration/SKILL.md
T
2026-07-12 10:17:17 -04:00

227 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 10pm2am (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, 10pm2am)
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.