tools/ha-service.mjs comble le trou des MCP disponibles : ha-get-state lit un etat, ha-set-helper ecrit un helper, mais aucun n'appelle un service. Or homeassistant.check_config et homeassistant.restart sont indispensables apres une modification de configuration.yaml (le recorder n'est pas rechargeable a chaud). Meme schema que tools/n8n-push.mjs : fetch, jamais curl, jeton lu dans .env, jamais en dur. Les services destructifs exigent --yes explicite. .env ajoute a .gitignore : il contenait HA_TOKEN et n'etait pas exclu, un git add -A l'aurait pousse sur le remote (le fichier n'etait pas encore suivi). Note : configuration.yaml (hors depot, sur le NAS) recoit recorder.purge_keep_days = 90 pour pouvoir rejouer un incident plusieurs semaines apres coup. Sauvegarde dans SAVE_CONFIG/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
85 lines
3.3 KiB
JavaScript
85 lines
3.3 KiB
JavaScript
#!/usr/bin/env node
|
|
// Usage: node tools/ha-service.mjs <domaine>.<service> [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 <domaine>.<service> [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));
|