initial commit

This commit is contained in:
ray
2026-07-12 10:17:17 -04:00
commit dab5a4ebc6
1424 changed files with 330463 additions and 0 deletions
@@ -0,0 +1,226 @@
---
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.