Files
EVCS/tools/arbitrage.mjs

106 lines
6.4 KiB
JavaScript

// =====================================================================
// ARBITRE ENCADRE (08/10/2026, decision du proprietaire)
//
// Pour les cas qu'aucune regle n'a prevus : le CODE detecte en direct une
// situation incoherente et formule une question a CHOIX FERMES ; OpenClaw
// (concierge, SPARK) l'envoie par WhatsApp ; la reponse du proprietaire est
// appliquee par le service d'historisation, et SEULEMENT l'ordre attache au
// choix retenu, pris dans la liste blanche du guichet d'ordres (ORDRES).
// Le modele ne decide rien et n'invente aucun choix : il relaie.
//
// Chaque arbitrage est historise (table `arbitrage`) : un cas qui revient avec
// la meme reponse deviendra une regle du planificateur, apres rejeu
// (docs/PLAN_PLANIFICATEUR_HC.md, §7).
//
// `detecterSituation` est pure (tests : tests/arbitrage/run.mjs).
// =====================================================================
export const DUREE_MIN = 10; // la situation doit durer 10 min
const VOITURE_ABSENTE_OU_PLEINE = /not_connected|done|full|complete|charged/i;
/**
* @param {object} e etat lu en direct
* fenetre : 'nuit' | 'midi' | null (fenetre HC en cours)
* finFenetre : 'HH:MM' | null
* contacteurOn, voitureBranchee : booleens
* borneW : puissance de la borne (W)
* sourceMode, etat, raison : controleur de la borne
* vehicule : selecteur (VW, JEEP, ZERO)
* modeNuit : 'sans_edf' | 'complete'
* sessionEngagee, manqueKwh, soc, batterieA, dvccA : recharge de la batterie (cas 2)
* verrouHysteresis, tensionEntreeV, reseauW, contacteurEtat : EDF demande mais absent (cas 0)
* @returns {null | { cas, question, choix: [{ cle, libelle, ordre: null | { ordre, valeur } }] }}
* choix vide = ALERTE : rien a decider, seulement a signaler.
*/
export function detecterSituation(e) {
// CAS 0 — ALERTE (13/10/2026) : EDF est DEMANDE (session HC engagee, ou verrou
// d'hysteresis) mais le reseau N'ARRIVE PAS. Nuit du 12-13/10 : session de
// nuit engagee a 01:24, ordre de fermeture envoye chaque minute, le
// disjoncteur connecte n'a jamais ferme (tension d'entree du MultiPlus a
// 0 V, aucun claquement ; HA affichait « on » par anticipation puis le module
// renvoyait « off ») : batterie 31 -> 26 % en 4 h 30, sans que personne le
// sache. Rearme au levier a 05:57, il a tenu. Ni la commande ni la mesure
// ne suffisent seules : on juge sur la TENSION reelle d'entree et la
// puissance reseau. Les 10 min de DUREE_MIN couvrent l'etablissement normal.
const edfDemande = (e.fenetre && e.sessionEngagee) || e.verrouHysteresis;
if (edfDemande && Number.isFinite(Number(e.tensionEntreeV)) && Number(e.tensionEntreeV) < 100
&& !(Number(e.reseauW) > 100)) {
const pourquoi = e.fenetre && e.sessionEngagee ? `la session d'heures creuses de ${e.fenetre} est engagee` : `l'hysteresis demande EDF (batterie basse)`;
return {
cas: 'edf_ne_ferme_pas',
question: `EDF ne se ferme pas : ${pourquoi}, mais le reseau n'arrive pas (tension d'entree ${Math.round(Number(e.tensionEntreeV))} V, contacteur affiche « ${e.contacteurEtat || 'inconnu'} »). `
+ `L'ordre de fermeture n'est pas execute par le disjoncteur EDF GENERAL OK (Wi-Fi ?). La batterie ${Number.isFinite(Number(e.soc)) ? `est a ${Math.round(Number(e.soc))} % et ` : ''}se vide : va le rearmer au levier, puis previens la session EVCS.`,
choix: [],
};
}
// CAS 1 — voiture branchee, non pleine, qui NE CHARGE PAS alors qu'EDF est deja
// ferme en heures creuses. Nuit du 07-08/10 : VW « bloquee » (sans EDF) de 01:24
// a 05:14. Elargi le 10/10 : HC de midi du 09/10, JEEP en « Reseau HC autorise »
// mais « conditions de demarrage non reunies » — quel que soit l'etat du
// controleur, une voiture branchee qui ne prend rien avec EDF ferme est une
// incoherence.
if (e.fenetre && e.contacteurOn && e.voitureBranchee && Number(e.borneW) < 100
&& !VOITURE_ABSENTE_OU_PLEINE.test(e.etat || '')) {
const v = e.vehicule || 'la voiture';
const nuit = e.fenetre === 'nuit';
const bloqueeSansEdf = /bloqu/i.test(e.sourceMode || '');
if (nuit && bloqueeSansEdf && e.modeNuit === 'complete') return null; // deja autorisee
// La nuit en « sans EDF », le levier est le mode de nuit ; sinon force_grid_now,
// qui expire seul a la fin des heures creuses.
const levierMode = nuit && bloqueeSansEdf;
return {
cas: 'voiture_bloquee_edf_ferme',
question: `${v} est branchee et ne charge pas, alors qu'EDF est ferme en heures creuses (jusqu'a ${e.finFenetre || 'la fin de la fenetre'}). `
+ `Raison du controleur : « ${String(e.raison || 'inconnue').split('|')[0].trim()} ». Que fais-je ?`,
choix: [
levierMode
? { cle: '1', libelle: `Charger ${v} sur EDF jusqu'a ${e.finFenetre || '07:24'} (mode de nuit « complete »)`, ordre: { ordre: 'mode_nuit', valeur: 'complete' } }
: { cle: '1', libelle: `Charger ${v} sur EDF jusqu'a ${e.finFenetre || '14:24'} (« force_grid_now », expire en fin d'heures creuses)`, ordre: { ordre: 'strategie', valeur: 'force_grid_now' } },
{ cle: '2', libelle: 'Laisser en attente (rien ne change)', ordre: null },
],
};
}
// CAS 2 — ALERTE (pas de choix) : la batterie a un manque a reprendre pendant
// une session HC, et elle recoit moins de la moitie du courant que l'ACM
// autorise. HC de midi du 09/10 : 32 A pour 140 A (consigne qui descendait,
// tension 50,1 V). Rien a decider pour le proprietaire : c'est un defaut a
// corriger PENDANT l'heure creuse, pas le lendemain dans la revue.
if (e.fenetre && e.contacteurOn && e.sessionEngagee && Number(e.manqueKwh) > 0.5
&& Number(e.soc) < 95 && Number(e.dvccA) > 20 && Number(e.batterieA) < 0.5 * Number(e.dvccA)) {
return {
cas: 'recharge_bridee',
question: `Recharge bridee en heures creuses de ${e.fenetre} : la batterie recoit ${Math.round(Number(e.batterieA))} A pour ${Math.round(Number(e.dvccA))} A autorises, `
+ `alors qu'il reste ${Math.round(Number(e.manqueKwh) * 10) / 10} kWh a reprendre (SOC ${Math.round(Number(e.soc))} %). Signale-le a la session EVCS pour qu'elle corrige avant ${e.finFenetre || 'la fin de la fenetre'}.`,
choix: [],
};
}
return null;
}
// Reponse du proprietaire : "1", "2", « 1 », « choix 2 »… -> cle ou null.
export function lireChoix(texte, choix) {
const m = /\b([1-9])\b/.exec(String(texte || ''));
if (!m) return null;
return choix.find((c) => c.cle === m[1]) || null;
}