--- 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... ``` 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 ``` `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='', prompt='Check using ha_get_state. If , call ha_call_service to 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.