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:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -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
84
tools/ha-service.mjs
Normal 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));
|
||||
Reference in New Issue
Block a user