#!/usr/bin/env node // Usage: node tools/ha-history.mjs [,...] [debut] [fin] // node tools/ha-history.mjs sensor.victron_battery_soc 2026-08-25T00:00 2026-08-25T10:00 // node tools/ha-history.mjs input_boolean.evcs_advisor_apply_night_target -48h // // Lit l'historique d'états via l'API REST de Home Assistant (jeton dans .env, // même mécanique que tools/ha-service.mjs). // // ⚠️ PIÈGE VÉRIFIÉ : /api/history/period ne renvoie QU'UNE SEULE JOURNÉE si // end_time est omis, sans le signaler. Ce script impose donc toujours les deux // bornes. // // Les MCP du projet ne donnent que l'état courant : pour toute analyse // a posteriori (qui a changé quoi, et quand), il faut l'historique. import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); function loadToken() { if (process.env.HA_TOKEN) return process.env.HA_TOKEN; const envPath = path.join(ROOT, '.env'); if (!fs.existsSync(envPath)) { console.error('Jeton introuvable : ni $HA_TOKEN, ni fichier .env.'); process.exit(1); } for (const line of fs.readFileSync(envPath, 'utf8').split('\n')) { const m = line.match(/^\s*HA_TOKEN\s*=\s*(.+?)\s*$/); if (m) return m[1].replace(/^["']|["']$/g, ''); } console.error('HA_TOKEN absent du fichier .env'); process.exit(1); } const HA_URL = process.env.HA_URL || 'http://10.10.10.17:8123'; const entities = (process.argv[2] || '').split(',').filter(Boolean); if (!entities.length) { console.error('Usage: node tools/ha-history.mjs [,...] [debut|-Nh] [fin]'); process.exit(1); } function parseBound(arg, fallback) { if (!arg) return fallback; const rel = arg.match(/^-(\d+)h$/); if (rel) return new Date(Date.now() - Number(rel[1]) * 3600e3); return new Date(arg); } const end = parseBound(process.argv[4], new Date()); const start = parseBound(process.argv[3], new Date(end.getTime() - 24 * 3600e3)); const url = `${HA_URL}/api/history/period/${start.toISOString()}` + `?end_time=${encodeURIComponent(end.toISOString())}` + `&filter_entity_id=${encodeURIComponent(entities.join(','))}` + `&minimal_response&significant_changes_only`; const res = await fetch(url, { headers: { Authorization: `Bearer ${loadToken()}` } }); if (!res.ok) { console.error(`HTTP ${res.status} — ${await res.text()}`); process.exit(1); } const series = await res.json(); const fmt = (iso) => new Date(iso).toLocaleString('fr-FR', { timeZone: 'Europe/Paris', day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', }); console.log(`Fenêtre : ${fmt(start.toISOString())} → ${fmt(end.toISOString())} (heure de Paris)\n`); for (const rows of series) { if (!rows.length) continue; console.log(`=== ${rows[0].entity_id} (${rows.length} points) ===`); let prev = null; for (const r of rows) { if (r.state === prev) continue; console.log(` ${fmt(r.last_changed || r.last_updated)} ${r.state}`); prev = r.state; } console.log(''); }