#!/usr/bin/env node // Usage: node tools/ha-service.mjs . [jsonData] // node tools/ha-service.mjs homeassistant.check_config // node tools/ha-service.mjs homeassistant.restart // node tools/ha-service.mjs input_number.set_value '{"entity_id":"...","value":30}' // // Appelle un service Home Assistant via son API REST. Le jeton est lu dans le // fichier .env du projet (HA_TOKEN) ou dans la variable d'environnement du même // nom — JAMAIS écrit en dur ici, et .env est exclu de git. // // Pourquoi ce tool : les MCP disponibles (ha-get-state, ha-set-helper, ...) ne // couvrent que la lecture d'état et l'écriture de helpers. Certaines opérations // d'administration — notamment `homeassistant.check_config` et // `homeassistant.restart`, indispensables après une modification de // configuration.yaml (le recorder n'est pas rechargeable à chaud) — n'ont aucun // équivalent MCP. On passe donc par l'API REST officielle, avec fetch (pas de // curl : le shell le bloque, et fetch est déjà le schéma utilisé par // tools/n8n-push.mjs). // // ⚠️ `homeassistant.restart` interrompt toutes les automatisations pendant // ~30 s. Le script demande donc une confirmation explicite : ajouter --yes. 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 à la racine du projet.'); 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 target = process.argv[2]; if (!target || !target.includes('.')) { console.error('Usage: node tools/ha-service.mjs . [jsonData] [--yes]'); process.exit(1); } const [domain, ...rest] = target.split('.'); const service = rest.join('.'); const rawData = process.argv[3] && !process.argv[3].startsWith('--') ? process.argv[3] : '{}'; let data; try { data = JSON.parse(rawData); } catch (e) { console.error('Données JSON invalides :', e.message); process.exit(1); } const DESTRUCTIF = new Set(['homeassistant.restart', 'homeassistant.stop']); if (DESTRUCTIF.has(`${domain}.${service}`) && !process.argv.includes('--yes')) { console.error(`"${domain}.${service}" interrompt Home Assistant. Relancer avec --yes pour confirmer.`); process.exit(1); } const res = await fetch(`${HA_URL}/api/services/${domain}/${service}`, { method: 'POST', headers: { Authorization: `Bearer ${loadToken()}`, 'Content-Type': 'application/json', }, body: JSON.stringify(data), }); const text = await res.text(); if (!res.ok) { console.error(`ÉCHEC ${res.status} ${res.statusText}`); console.error(text.slice(0, 800)); process.exit(1); } console.log(`OK ${domain}.${service} → ${res.status}`); if (text.trim() && text.trim() !== '[]') console.log(text.slice(0, 800));