diff --git a/.gitignore b/.gitignore index 0cf7b66..90a4611 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ modified/prepush-*.json # Sorties temporaires node_modules/ .claude/settings.local.json + +# Jeton HA local (ne jamais versionner) +.env diff --git a/tools/ha-service.mjs b/tools/ha-service.mjs new file mode 100644 index 0000000..83efb11 --- /dev/null +++ b/tools/ha-service.mjs @@ -0,0 +1,84 @@ +#!/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));