#!/usr/bin/env node // CALIBRATION DES REGLES D ANOMALIE — LECTURE SEULE, rejouable. // // node tools/calibrer-anomalies.mjs // // On ne fige pas un seuil au juge. Les six oscillateurs de ce projet sont tous // nes de constantes posees « au pif » (cf. la memoire du 24/08). Un seuil se // CHOISIT sur l historique, en exigeant qu il SEPARE le cas pathologique connu // des bascules normales — et s il ne separe pas, on ne le fige pas. // // A REJOUER apres toute evolution du parc ou des workflows : la cadence des // automates change les ecarts, donc les seuils. // On ne fige pas un seuil au juge : on le choisit sur l historique, en exigeant // qu il SEPARE le cas pathologique connu (la salve du 12/09) des bascules // normales du cycle jour/nuit. import fs from 'fs'; import { execFileSync } from 'child_process'; const env = fs.readFileSync('.env', 'utf8'); const mdp = (env.match(/^PG_LECTURE_PASSWORD=(.*)$/m)?.[1] || '').trim().replace(/^["']|["']$/g, ''); const sql = (q) => execFileSync('psql', ['-h','10.10.10.22','-p','5433','-U','evcs_lecture','-d','evcs','-At','-F','\t','-c',q], { env: { ...process.env, PGPASSWORD: mdp }, encoding: 'utf8' }) .trim().split('\n').filter(Boolean).map(l => l.split('\t')); const ENTITES = sql(`select detail->>'entite', count(*) from evenement where detail ? 'avant' group by 1 having count(*) >= 10 order by 2 desc`); // Un episode = N bascules dans une fenetre glissante de T secondes. function episodes(ts, N, T) { const out = []; for (let i = 0; i + N - 1 < ts.length; i++) { if (ts[i + N - 1] - ts[i] <= T * 1000) { const dernier = out[out.length - 1]; // On etend l episode en cours au lieu d en ouvrir un par fenetre. if (dernier && ts[i] - dernier.fin <= T * 1000) { dernier.fin = ts[i + N - 1]; dernier.n++; } else out.push({ debut: ts[i], fin: ts[i + N - 1], n: N }); } } return out; } const fr = (ms) => new Date(ms).toLocaleString('fr-FR', { timeZone: 'Europe/Paris', day:'2-digit', month:'2-digit', hour:'2-digit', minute:'2-digit' }); console.log('ENTITE bascules plus petit ecart'); console.log('-'.repeat(78)); const donnees = new Map(); for (const [e, n] of ENTITES) { const ts = sql(`select extract(epoch from ts)*1000 from evenement where detail->>'entite'='${e}' order by ts`).map(r => Number(r[0])); donnees.set(e, ts); let min = Infinity; for (let i = 1; i < ts.length; i++) min = Math.min(min, (ts[i] - ts[i-1]) / 1000); console.log(`${e.slice(0,44).padEnd(46)} ${String(n).padStart(5)} ${Math.round(min)} s`); } console.log('\n\nCE QUE CHAQUE COUPLE (N, T) DECLENCHE\n' + '='.repeat(78)); for (const [N, T] of [[3,600],[3,300],[4,600],[2,300],[3,900]]) { console.log(`\n--- ${N} bascules en ${T/60} min ---`); for (const [e, ts] of donnees) { const eps = episodes(ts, N, T); const court = e.replace(/^(switch|input_boolean|input_text|binary_sensor|sensor)\./,''); if (!eps.length) { console.log(` ${court.slice(0,40).padEnd(42)} muet`); continue; } console.log(` ${court.slice(0,40).padEnd(42)} ${eps.length} episode(s)`); for (const x of eps.slice(0, 4)) { console.log(` ${fr(x.debut)} -> ${fr(x.fin)} ${x.n} bascules en ${Math.round((x.fin-x.debut)/1000)} s`); } if (eps.length > 4) console.log(` … et ${eps.length - 4} autre(s)`); } }