Initial commit: Hermes Agent Skills collection
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
# HA Browser Console Access (No Local Token Needed)
|
||||
|
||||
When no HA API token is available locally (`.env` missing, 1Password inaccessible, token redacted after compaction), the HA web UI's browser session can be used to query the REST API directly. This is a reliable fallback that avoids needing to create/manage long-lived tokens.
|
||||
|
||||
## Technique
|
||||
|
||||
1. **Navigate to HA web UI** via `browser_navigate` to `http://10.0.30.10:8123` (internal) or `https://homeassistant.familie-schoen.com` (external).
|
||||
2. **Log in** with username/password via the login form (credentials in memory: dominik / 28acaneltO!).
|
||||
3. **Extract the bearer token** from the browser console:
|
||||
|
||||
```javascript
|
||||
(async () => {
|
||||
const conn = await window.hassConnection; // It's a Promise!
|
||||
const token = conn.auth.data.access_token;
|
||||
return 'Token len: ' + token.length + ' | prefix: ' + token.substring(0, 20);
|
||||
})()
|
||||
```
|
||||
|
||||
Key details:
|
||||
- `window.hassConnection` is a **Promise** (not a plain object). Must `await` it.
|
||||
- Resolves to `{ auth, conn }`.
|
||||
- Token is at `conn.auth.data.access_token` (NOT `accessToken` — camelCase key is `access_token`).
|
||||
- Token is a JWT (~183 chars, starts with `eyJhbG...`).
|
||||
- Token expires (refresh handled by HA frontend automatically). If a query fails with 401, re-extract the token.
|
||||
|
||||
4. **Query REST API via `fetch()`** in `browser_console`:
|
||||
|
||||
```javascript
|
||||
(async () => {
|
||||
const conn = await window.hassConnection;
|
||||
const token = conn.auth.data.access_token;
|
||||
|
||||
// Get all states
|
||||
const resp = await fetch('/api/states', {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
const states = await resp.json();
|
||||
|
||||
// Filter for relevant entities
|
||||
const powerStates = states.filter(s => {
|
||||
const eid = s.entity_id.toLowerCase();
|
||||
return eid.includes('power') || eid.includes('netz') || /* ... */;
|
||||
}).map(s => ({ eid: s.entity_id, state: s.state, unit: s.attributes.unit_of_measurement }));
|
||||
|
||||
return JSON.stringify(powerStates);
|
||||
})()
|
||||
```
|
||||
|
||||
5. **Query history** with time ranges:
|
||||
|
||||
```javascript
|
||||
(async () => {
|
||||
const conn = await window.hassConnection;
|
||||
const token = conn.auth.data.access_token;
|
||||
const entities = 'sensor.evcc_grid_power,sensor.evcc_home_power';
|
||||
|
||||
const url = '/api/history/period/2026-07-09T03:00:00?filter_entity_id=' + entities +
|
||||
'&end_time=2026-07-09T06:00:00&minimal_response';
|
||||
const resp = await fetch(url, {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
const data = await resp.json();
|
||||
// data is an array of arrays — one per entity, each containing state-change objects
|
||||
// Each state object: { state, last_changed, attributes }
|
||||
|
||||
// Bucket by time interval for summarization
|
||||
const buckets = {};
|
||||
for (const h of data[0]) {
|
||||
const t = h.last_changed.substring(11, 16); // HH:MM
|
||||
const bucket = t.substring(0, 3) + (parseInt(t.substring(3,5)) >= 30 ? '30' : '00');
|
||||
const v = parseFloat(h.state);
|
||||
if (isNaN(v)) continue;
|
||||
if (!buckets[bucket]) buckets[bucket] = [];
|
||||
buckets[bucket].push(v);
|
||||
}
|
||||
|
||||
return JSON.stringify(
|
||||
Object.entries(buckets).map(([time, vals]) => ({
|
||||
time,
|
||||
avg: (vals.reduce((a,b)=>a+b,0)/vals.length).toFixed(0),
|
||||
max: Math.max(...vals).toFixed(0)
|
||||
})).sort((a,b) => a.time.localeCompare(b.time))
|
||||
);
|
||||
})()
|
||||
```
|
||||
|
||||
## Advantages Over Other Methods
|
||||
|
||||
| Method | Requires | Limitations |
|
||||
|--------|----------|-------------|
|
||||
| REST API (curl/terminal) | Local token in `.env` or 1Password | Token may be missing/redacted |
|
||||
| WebSocket (Python) | Local token + Python websocket lib | Heavy setup, token redaction in terminal |
|
||||
| Proxmox guest exec | SSH to proxmox1 | Guest agent often down |
|
||||
| **Browser console** | **Browser + HA login** | **Always works if web UI is reachable** |
|
||||
|
||||
## Alternative Token Extraction (When hassConnection Goes Stale)
|
||||
|
||||
After re-login or long browser sessions, `window.hassConnection` may resolve to an empty object or `conn.auth` becomes `undefined`. Use this fallback instead — it accesses the `hass` object directly from the DOM element (no Promise to await):
|
||||
|
||||
```javascript
|
||||
(async () => {
|
||||
const ha = document.querySelector("home-assistant");
|
||||
const token = ha.hass.auth.accessToken; // Direct property, NOT a Promise
|
||||
|
||||
const resp = await fetch('/api/states', {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
return JSON.stringify(await resp.json());
|
||||
})()
|
||||
```
|
||||
|
||||
Key differences from the `hassConnection` approach:
|
||||
- `hass.auth.accessToken` — direct property on the `home-assistant` custom element's `hass` object. No await needed.
|
||||
- `hass.auth` has keys: `["data", "_saveTokens"]`. The `accessToken` getter returns the JWT string.
|
||||
- If `document.querySelector("home-assistant")?.hass` is undefined, the page hasn't fully loaded. Wait and retry, or re-navigate.
|
||||
- The `hass.connection` object (WebSocket connection) is at `hass.hass.connection` — but its `auth` property is NOT populated. Use `hass.hass.auth` instead.
|
||||
|
||||
**When to use which:**
|
||||
| Scenario | Method |
|
||||
|----------|--------|
|
||||
| Fresh page load, quick query | `(await window.hassConnection).auth.data.access_token` |
|
||||
| After re-login, stale session | `document.querySelector("home-assistant").hass.auth.accessToken` |
|
||||
| `hassConnection` returns empty `{}` | Same fallback — DOM element approach |
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- **`window.hassConnection` is a Promise, not an object.** Calling `Object.keys(window.hassConnection)` returns empty because it's a Promise. Must `await` it first.
|
||||
- **Token key is `access_token` (snake_case), not `accessToken` (camelCase).** The `auth.data` object uses snake_case: `access_token`, `refresh_token`, `token_type`, `expires_in`.
|
||||
- **Multi-line JSON in `browser_console` expression.** The `browser_console` tool evaluates a single JavaScript expression. Multi-statement code must be wrapped in an IIFE: `(async () => { ... })()`. Pretty-printing with `JSON.stringify(obj, null, 2)` can cause parse errors in some cases — use `JSON.stringify(obj)` (no spacing) for reliability.
|
||||
- **`browser_console` returns truncated results for large outputs.** If the result exceeds ~5000 chars, it may be truncated. Keep queries focused — filter entities before returning, or paginate.
|
||||
- **History API returns array-of-arrays.** `/api/history/period/{timestamp}?filter_entity_id=A,B,C` returns `[ [states_for_A], [states_for_B], [states_for_C] ]`. Each inner array contains state-change records with `{ state, last_changed, last_updated, attributes }`.
|
||||
- **Use `&minimal_response` for history queries.** Without it, each state record includes full attributes, making responses enormous. With it, attributes are stripped — sufficient for numerical analysis.
|
||||
- **`window.hassConnection` goes stale during long browser sessions.** After ~10-15 minutes of inactivity (or after the browser tab loses focus), the HA frontend's WebSocket connection drops and `window.hassConnection` may resolve to an empty object `{}` (keys: none) or `conn.auth` becomes `undefined`. Symptom: `TypeError: Cannot read properties of undefined (reading 'auth')`. **Fix**: re-navigate to the HA URL (`browser_navigate` to `http://10.0.30.10:8123`), wait 5s for the page to load, re-login if the login form appears, then re-extract the token. After re-login, verify the page has fully loaded before accessing `hassConnection` — check `typeof (await window.hassConnection).auth !== 'undefined'`.
|
||||
- **HA History API returns data from BEFORE the specified start timestamp.** A query with `start_time=2026-07-09T04:15:00` returned records dating back to `02:15` — a full 2 hours earlier. This appears to be by design (the API returns the state at the start time plus all changes within the window, but the "state at start time" may be sourced from much earlier records). **Impact**: when doing time-windowed analysis (e.g. "what happened at 04:30?"), filter the results client-side by timestamp AFTER fetching, or the dataset will be polluted with irrelevant earlier data. Example: `hist.filter(h => h.last_changed.substring(11,16) >= '04:15')`.
|
||||
- **History API `end_time` parameter does NOT reliably truncate results.** Even with `end_time` set, the API may return records beyond that timestamp. Always filter client-side for precise time windows.
|
||||
Reference in New Issue
Block a user