Files
hermes-skills/smart-home/smart-home-automation/references/ha-browser-console-access.md
T

8.0 KiB

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:
(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.
  1. Query REST API via fetch() in browser_console:
(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);
})()
  1. Query history with time ranges:
(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):

(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.