Outil d'appel de service HA (redemarrage, check_config) + protection du jeton

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
This commit is contained in:
Tallagrand Thierry
2026-08-24 14:10:10 +02:00
parent 057e0f7d8f
commit 12e8bddb98
2 changed files with 87 additions and 0 deletions

3
.gitignore vendored
View File

@@ -5,3 +5,6 @@ modified/prepush-*.json
# Sorties temporaires
node_modules/
.claude/settings.local.json
# Jeton HA local (ne jamais versionner)
.env

84
tools/ha-service.mjs Normal file
View File

@@ -0,0 +1,84 @@
#!/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));