Calibration des regles d anomalie : trois tiennent, une ne tient pas

L oscillation rapide se calibre sans ambiguite : sur le contacteur reseau, le
plus grand ecart pathologique vaut 193 s et le plus petit ecart normal 857 s,
facteur 4,4 sans aucune valeur entre les deux. Quatre bascules en dix minutes
isole exactement la salve du 12/09 et reste muet sur dix entites pendant dix
jours.

Piege trouve en calibrant : les plus petits ecarts normaux de plusieurs entites
valent 242 a 320 s, c est-a-dire la cadence des workflows. Ce ne sont pas des
oscillations mais deux executions consecutives qui changent d avis — toute
regle a deux bascules en cinq minutes se declenche sur le fonctionnement normal.

En revanche la regle des sessions reseau repetees NE SE CALIBRE PAS : les ecarts
entre fermetures forment un continuum de 4 minutes a 24 heures, sans
discontinuite. Or c est la pathologie la plus couteuse des deux. Je ne pose pas
de seuil dessus : elle demande une formulation metier et plus d historique, et
la bacler serait refaire l erreur que ce projet a payee six fois.

La regle de la mesure figee, elle, se valide sur un incident reel : le 05/09 la
tension et le courant batterie se sont tus 27 minutes pendant que le SOC, issu
du meme flux, publiait toutes les deux minutes. Le discriminant est relatif, pas
absolu — un silence long est normal pour une grandeur intermittente.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj
This commit is contained in:
Tallagrand Thierry
2026-09-14 03:22:18 +02:00
parent 3b73e10277
commit 60165e139b
2 changed files with 155 additions and 0 deletions

View File

@@ -0,0 +1,86 @@
# Calibration des regles d anomalie — 14/09/2026
Rejouable : `node tools/calibrer-anomalies.mjs`. Historique disponible : du
04/09 (creation de la table `evenement`) au 14/09, dix entites suivies.
## Regle 1 — oscillation rapide : CALIBREE, sans ambiguite
Les 41 bascules du contacteur reseau se separent sans recouvrement :
| | valeur |
|---|---|
| plus grand ecart **pathologique** (salve du 12/09) | **193 s** |
| plus petit ecart **normal** | **857 s** |
Un facteur 4,4 entre les deux, aucune valeur entre. Balayage des couples
(N bascules, T fenetre) sur les dix entites :
| seuil | ce qui sort |
|---|---|
| 2 en 5 min | **19 episodes** sur 8 entites — inutilisable |
| 3 en 15 min | 11 episodes sur 6 entites |
| 3 en 10 min | la salve du 12/09 + 3 faux positifs |
| **4 en 10 min** | **la salve du 12/09, et RIEN d autre** |
| 3 en 5 min | idem, mais moins de marge |
**Retenu : 4 bascules en 10 minutes.** Il isole exactement
`12/09 06:25 -> 06:35, 6 bascules en 579 s` et reste muet sur les dix entites
pendant dix jours.
⚠️ **Piege trouve en calibrant** : les plus petits ecarts « normaux » de
plusieurs entites valent **242, 249, 251, 258, 320 s** — c est-a-dire ~5 minutes,
la CADENCE des workflows du bilan et de l ACM. Ce ne sont pas des oscillations,
ce sont deux executions consecutives qui changent d avis. Toute regle avec
N=2 et T <= 5 min se declenche donc sur le fonctionnement normal.
## Regle 2 — sessions reseau repetees : NON CALIBRABLE EN L ETAT
Le second oscillateur connu est LENT : la nuit du 26-27/08 comptait trois
sessions reseau (2,5 kWh importes), celle du 20-21/08 six, avec une periode
d environ trente minutes. **Une fenetre de dix minutes ne peut pas le voir.**
La pathologie est bien presente dans les donnees actuelles : 7 fermetures du
contacteur le 11/09, dont quatre le soir (16:58, 20:13, 22:39, 23:43) hors de
toute salve rapide, et 3 le 06/09 — dont deux sont le defaut documente de la
regle de protection, corrige le meme jour.
Mais les ecarts entre fermetures forment un **continuum** :
1155 · 196 · 732 · 87 · 702 · 1413 · 1476 · 1391 · 1459 · 963 · 195 ·
147 · 64 · 403 · 4 · 4 · 542 · 282 · 306 (minutes)
De 4 minutes a 24 heures sans discontinuite. Tout seuil « N fermetures en H
heures » y serait arbitraire, et melangerait les sessions de nuit avec les
fermetures diurnes de la regle de protection, qui sont une autre affaire.
**Decision : ne PAS figer cette regle.** Elle demande une formulation metier —
« plus d une session par fenetre HC », ou « re-fermeture alors que les
conditions du relachement tiennent toujours » — et davantage d historique. La
poser au juge serait exactement l erreur que ce projet a deja payee six fois.
## Regle 3 — mesure figee : CALIBRABLE, validee sur un incident reel
L incident du 05/09 (HANDOFF §57) est parfaitement visible :
| grandeur | plus long trou, 20:30-22:30 | points |
|---|---:|---:|
| `battery_voltage` | **27 min** | 92 |
| `battery_current` | **27 min** | 94 |
| `battery_soc` | 2 min | 119 |
| `consumption_power_l1` | 1 min | 119 |
Le discriminant n est PAS le silence absolu — l ecart median de toutes les
grandeurs vaut 61-63 s, mais le maximum atteint **22 heures** pour la puissance
de la borne, et c est normal : le service n enregistre que les changements, et
un vehicule debranche ne change pas. Le discriminant est **relatif** : une
grandeur se tait pendant qu une SOEUR de la meme source continue de publier.
Le 05/09, le courant batterie s est tu 27 minutes — 26 fois sa cadence — quand
le SOC, issu du meme flux MQTT Victron, publiait toutes les deux minutes.
## Ce que la calibration change au plan
Le plan approuve prevoyait quatre regles. **Trois sont construisibles**
(oscillation rapide, mesure figee, engagement non relache) ; la quatrieme —
sessions repetees — ne l est pas sans formulation metier. C est la plus
couteuse des deux pathologies d oscillation (2,5 kWh contre ~0,5), donc elle
merite son propre chantier plutot qu un seuil pose a la hate.

View File

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