|
|
|
|
@@ -1,317 +1,80 @@
|
|
|
|
|
// =====================================================
|
|
|
|
|
// EVCS ADVISOR SHADOW V1 — BUILD_PROMPT
|
|
|
|
|
// Construit le prompt LLM à partir du snapshot HA compact.
|
|
|
|
|
// MODE OMBRE : ce workflow n'écrit que des helpers advisor_*.
|
|
|
|
|
// EVCS ADVISOR — BUILD_PROMPT (RELECTEUR DE LA JOURNEE, 08/10/2026)
|
|
|
|
|
//
|
|
|
|
|
// Le conseiller n'observe plus l'instantane toutes les 15 min : 1 500 « rien a
|
|
|
|
|
// signaler » contre 871 signalements sans suite, et aucun des rates du 07/10
|
|
|
|
|
// (HC de midi perdue, EDF en heures pleines a 18:03, VW bloquee de 01:24 a
|
|
|
|
|
// 05:14 avec EDF ferme) n'avait ete vu. Il RELIT desormais, une fois par jour,
|
|
|
|
|
// les constats CALCULES par le service d'historisation (tools/revue-jour.mjs,
|
|
|
|
|
// sensor.evcs_revue_jour) et propose pour chacun une piste a examiner.
|
|
|
|
|
// Doctrine §79 : les chiffres viennent du code, le modele ne fait que relire.
|
|
|
|
|
// Il ne pilote rien.
|
|
|
|
|
//
|
|
|
|
|
// Il tourne quand : le conseiller est actif, la revue du jour est publiee, il
|
|
|
|
|
// y a au moins un point a examiner, et il n'a pas deja repondu aujourd'hui.
|
|
|
|
|
// =====================================================
|
|
|
|
|
|
|
|
|
|
function num(v, d) {
|
|
|
|
|
const n = Number.parseFloat(v);
|
|
|
|
|
return Number.isFinite(n) ? n : d;
|
|
|
|
|
}
|
|
|
|
|
function onOff(v) {
|
|
|
|
|
return (v ?? 'off').toString().trim().toLowerCase() === 'on';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const raw = $input.first().json;
|
|
|
|
|
const input = typeof raw.data === 'string' ? JSON.parse(raw.data) : (raw.data || raw);
|
|
|
|
|
const onOff = (v) => (v ?? 'off').toString().trim().toLowerCase() === 'on';
|
|
|
|
|
|
|
|
|
|
const jourParis = new Date().toLocaleDateString('sv-SE', { timeZone: 'Europe/Paris' });
|
|
|
|
|
let constats = input.revue_jour_constats;
|
|
|
|
|
if (typeof constats === 'string') { try { constats = JSON.parse(constats); } catch { constats = []; } }
|
|
|
|
|
if (!Array.isArray(constats)) constats = [];
|
|
|
|
|
const aExaminer = constats.filter((c) => c && c.gravite === 'a_examiner' && c.code && c.texte);
|
|
|
|
|
|
|
|
|
|
const revueDuJour = (input.revue_jour_date || '').toString().trim() === jourParis;
|
|
|
|
|
const dejaRepondu = (input.advisor_revue_date || '').toString().trim() === jourParis;
|
|
|
|
|
// Pas de relance au-dela de 10 h : un modele qui echoue ne doit pas etre
|
|
|
|
|
// rappele toute la journee (le courriel part a 08:15 sans lui).
|
|
|
|
|
const heureParis = Number(new Intl.DateTimeFormat('en-GB', { timeZone: 'Europe/Paris', hour: '2-digit', hour12: false }).format(new Date()));
|
|
|
|
|
|
|
|
|
|
const ctx = {
|
|
|
|
|
advisorEnabled: onOff(input.advisor_enabled),
|
|
|
|
|
agreeCount: num(input.advisor_agree_count, 0),
|
|
|
|
|
divergeCount: num(input.advisor_diverge_count, 0),
|
|
|
|
|
|
|
|
|
|
soc: num(input.soc_percent, NaN),
|
|
|
|
|
batteryCurrentA: num(input.battery_current_a, NaN),
|
|
|
|
|
pvW: num(input.pv_power_w, 0),
|
|
|
|
|
pvAvg10mW: num(input.pv_power_10m_avg_w, 0),
|
|
|
|
|
houseW: num(input.house_ac_power_w, 0),
|
|
|
|
|
gridOn: onOff(input.grid_state),
|
|
|
|
|
acinAvailable: num(input.acin1_available, 0) === 1,
|
|
|
|
|
|
|
|
|
|
vehicleConnected: onOff(input.evcs_vehicle_connected),
|
|
|
|
|
evcsCharging: onOff(input.evcs_charging),
|
|
|
|
|
evcsCurrentA: num(input.evcs_current_a, 0),
|
|
|
|
|
surplusState: (input.evcs_surplus_state || '').toString(),
|
|
|
|
|
sourceMode: (input.evcs_source_mode || '').toString(),
|
|
|
|
|
|
|
|
|
|
userStrategy: (input.user_strategy || 'auto_solar_plus').toString().trim().toLowerCase(),
|
|
|
|
|
allowGridHcNight: onOff(input.allow_grid_hc_night),
|
|
|
|
|
allowGridHcMidday: onOff(input.allow_grid_hc_midday),
|
|
|
|
|
nightTargetActual: num(input.night_target_soc_actual, 35),
|
|
|
|
|
currentMaxAutoA: num(input.current_max_auto_a, 22),
|
|
|
|
|
plafondEdfOnA: num(input.plafond_edf_on_a, 20),
|
|
|
|
|
|
|
|
|
|
departureTime: (input.departure_time || '').toString(),
|
|
|
|
|
departurePriority: onOff(input.departure_priority_enabled),
|
|
|
|
|
requiredEnergyKwh: num(input.required_energy_kwh, 0),
|
|
|
|
|
|
|
|
|
|
todayRemainingKwh: num(input.today_remaining_kwh, 0),
|
|
|
|
|
tomorrowKwh: num(input.tomorrow_kwh, 0),
|
|
|
|
|
|
|
|
|
|
// ── BILAN ENERGETIQUE ────────────────────────────────────────────────
|
|
|
|
|
// Ces grandeurs REMPLACENT les regles de seuil. Elles n'etaient pas
|
|
|
|
|
// transmises jusqu'au 12/09 : le conseiller raisonnait sur des paliers
|
|
|
|
|
// que le bilan avait deja demontres faux, et il ne pouvait pas mieux faire.
|
|
|
|
|
balanceStatus: (input.balance_status || 'indisponible').toString().trim(),
|
|
|
|
|
balanceLastRun: (input.balance_last_run || '').toString().trim(),
|
|
|
|
|
balanceRequiredSoc: num(input.balance_required_soc, NaN),
|
|
|
|
|
balanceRequiredSocPrudent: num(input.balance_required_soc_prudent, NaN),
|
|
|
|
|
balanceEnergyToCoverKwh: num(input.balance_energy_to_cover_kwh, NaN),
|
|
|
|
|
balanceCrossingHour: (input.balance_crossing_hour || '').toString().trim(),
|
|
|
|
|
balanceGridNeeded: onOff(input.balance_grid_needed),
|
|
|
|
|
balanceGridEnergyNeededKwh: num(input.balance_grid_energy_needed_kwh, NaN),
|
|
|
|
|
balancePvCensored: onOff(input.balance_pv_censored),
|
|
|
|
|
balanceCensorReason: (input.balance_censor_reason || '').toString().trim(),
|
|
|
|
|
balanceStorageShortfallKwh: num(input.balance_storage_shortfall_kwh, NaN),
|
|
|
|
|
balanceNextDeficitKwh: num(input.balance_next_deficit_kwh, NaN),
|
|
|
|
|
balanceNextDeficitWindow: (input.balance_next_deficit_window || '').toString().trim(),
|
|
|
|
|
stormLevel: (input.balance_storm_level || 'indisponible').toString().trim(),
|
|
|
|
|
stormReserveSoc: num(input.balance_storm_reserve_soc, NaN),
|
|
|
|
|
|
|
|
|
|
// ── PARC ET MATERIEL, MESURES ────────────────────────────────────────
|
|
|
|
|
modulesEnService: num(input.modules_en_service, 4),
|
|
|
|
|
bmsMaxDischargeA: num(input.bms_max_discharge_current_a, NaN),
|
|
|
|
|
bmsMaxChargeA: num(input.bms_max_charge_current_a, NaN),
|
|
|
|
|
gridSetpointW: num(input.grid_setpoint_w, NaN),
|
|
|
|
|
vehicleSelected: (input.vehicle_selected || 'inconnu').toString().trim(),
|
|
|
|
|
// Historique recent des transitions, fourni par le snapshot. C est la SUITE
|
|
|
|
|
// des evenements qui revele les contradictions entre automates ; un
|
|
|
|
|
// instantane ne peut pas les montrer.
|
|
|
|
|
evenements: String(input.evenements_recents || '')
|
|
|
|
|
.split('|').map(x => x.trim()).filter(Boolean).slice(0, 12)
|
|
|
|
|
.map(x => `- ${x}`),
|
|
|
|
|
|
|
|
|
|
mppt: (() => {
|
|
|
|
|
const modes = String(input.mppt_operation_modes || '')
|
|
|
|
|
.split(',').map(x => x.trim().toLowerCase())
|
|
|
|
|
.filter(x => x && x !== 'unavailable' && x !== 'unknown');
|
|
|
|
|
return {
|
|
|
|
|
total: modes.length,
|
|
|
|
|
aFond: modes.filter(x => x === 'mppt_active').length,
|
|
|
|
|
brides: modes.filter(x => x.includes('limited')).length,
|
|
|
|
|
};
|
|
|
|
|
})(),
|
|
|
|
|
jour: jourParis,
|
|
|
|
|
revueDuJour,
|
|
|
|
|
dejaRepondu,
|
|
|
|
|
constats: aExaminer.map((c) => ({ code: c.code, texte: String(c.texte), contexte: String(c.contexte || '') })),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Capacite REELLEMENT livree a la maison, deduite de la courbe mesuree le
|
|
|
|
|
// 03/09 sur 576 paliers et du parc declare. Un US5000 compte pour DEUX
|
|
|
|
|
// equivalents US2000C. Aucune constante : si le parc change, ceci suit.
|
|
|
|
|
// GRANDEURS DERIVEES — on les CALCULE pour lui (12/09/2026, iteration 2).
|
|
|
|
|
// Campagne de rejeu : sur le releve du 03/09 19:44 (PV 758 W, maison 1202 W)
|
|
|
|
|
// il a recommande de charger la voiture a 28 A. Les deux nombres etaient dans
|
|
|
|
|
// son prompt ; il n a pas fait la soustraction. Et sur celui du 05/09 20:40
|
|
|
|
|
// (batterie a -97,4 A pour une limite declaree de 100 A) il a repondu « SOC
|
|
|
|
|
// 79 % est suffisant » sans mentionner ni le courant ni la limite.
|
|
|
|
|
// Un modele raisonne bien sur UN nombre, mal sur deux qu il doit combiner.
|
|
|
|
|
// Meme principe que le bilan : on lui donne la grandeur, pas les ingredients.
|
|
|
|
|
const maisonHorsVoitureW = Math.max(0, ctx.houseW - (ctx.evcsCharging ? ctx.evcsCurrentA * 230 : 0));
|
|
|
|
|
const excedentSolaireW = Math.round(ctx.pvW - maisonHorsVoitureW);
|
|
|
|
|
const margeRechargeW = ctx.gridOn && Number.isFinite(ctx.gridSetpointW)
|
|
|
|
|
? Math.round(ctx.gridSetpointW - maisonHorsVoitureW)
|
|
|
|
|
: null;
|
|
|
|
|
// Le bilan dit lui-meme si la batterie doit monter. A defaut de bilan
|
|
|
|
|
// exploitable on ne tranche pas, et la marge reste un simple constat.
|
|
|
|
|
const batterieDoitMonter = Number.isFinite(ctx.balanceRequiredSoc) && Number.isFinite(ctx.soc)
|
|
|
|
|
? ctx.soc < ctx.balanceRequiredSoc
|
|
|
|
|
: null;
|
|
|
|
|
const margeDechargeA = Number.isFinite(ctx.bmsMaxDischargeA) && Number.isFinite(ctx.batteryCurrentA)
|
|
|
|
|
? Math.round(ctx.bmsMaxDischargeA - Math.abs(Math.min(0, ctx.batteryCurrentA)))
|
|
|
|
|
: null;
|
|
|
|
|
const partDuPlafond = Number.isFinite(ctx.bmsMaxDischargeA) && ctx.bmsMaxDischargeA > 0
|
|
|
|
|
? Math.round(100 * Math.abs(Math.min(0, ctx.batteryCurrentA)) / ctx.bmsMaxDischargeA)
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
const kwhParPointHaut = 0.169 * ctx.modulesEnService / 7;
|
|
|
|
|
const kwhParPointBas = 0.097 * ctx.modulesEnService / 7;
|
|
|
|
|
const capaciteUtileKwh = (67 * kwhParPointHaut + 11 * kwhParPointBas) * 0.955;
|
|
|
|
|
|
|
|
|
|
// Heure Paris
|
|
|
|
|
const parisFmt = new Intl.DateTimeFormat('fr-FR', {
|
|
|
|
|
timeZone: 'Europe/Paris',
|
|
|
|
|
weekday: 'long', day: '2-digit', month: '2-digit',
|
|
|
|
|
hour: '2-digit', minute: '2-digit', hour12: false,
|
|
|
|
|
});
|
|
|
|
|
const nowParis = parisFmt.format(new Date());
|
|
|
|
|
|
|
|
|
|
const systemPrompt = [
|
|
|
|
|
"Tu es le PLANIFICATEUR ENERGIE d'une maison en autoconsommation, en Bretagne.",
|
|
|
|
|
'Tu es en MODE OBSERVATION : tes reponses sont enregistrees et comparees au reel,',
|
|
|
|
|
'elles ne pilotent rien.',
|
|
|
|
|
"Tu es le RELECTEUR du systeme energie d'une maison en Bretagne : 16 kWc de solaire,",
|
|
|
|
|
'batterie de ~23 kWh utiles (0,30 kWh par point de SOC), Victron ESS SANS injection',
|
|
|
|
|
'(le solaire est bride quand personne ne le prend), abonnement EDF 12 kVA, contacteur',
|
|
|
|
|
'EDF ouvert la plupart du temps, heures creuses 01:24-07:24 et 12:24-14:24, borne de',
|
|
|
|
|
'recharge pour trois vehicules. Des automates decident : le controleur de la borne,',
|
|
|
|
|
"le bilan energetique (SOC requis), l'ACM (courant de charge), l'hysteresis EDF, des",
|
|
|
|
|
'automatisations Home Assistant.',
|
|
|
|
|
'',
|
|
|
|
|
'INSTALLATION, CHIFFRES MESURES (pas des ordres de grandeur) :',
|
|
|
|
|
`- Batterie : ${ctx.modulesEnService} equivalents US2000C, soit ${capaciteUtileKwh.toFixed(1)} kWh`,
|
|
|
|
|
` reellement livres a la maison entre 100 % et le plancher de securite de 22 %.`,
|
|
|
|
|
` Un point de SOC vaut ${Math.round(kwhParPointHaut * 1000)} Wh au-dessus de 33 %,`,
|
|
|
|
|
` ${Math.round(kwhParPointBas * 1000)} Wh en dessous.`,
|
|
|
|
|
'- Champ PV 16,1 kWc sur QUATRE orientations (sud-est 40, sud 35, ouest 45, un suiveur).',
|
|
|
|
|
'- Victron ESS, AUCUNE injection possible : la production est bridee des que rien',
|
|
|
|
|
' ne peut l absorber. Une PV mesuree basse ne veut donc PAS dire ressource faible.',
|
|
|
|
|
'- Borne EVCS 6-28 A. Reseau EDF commutable, heures creuses 01:24-07:24 et 12:24-14:24.',
|
|
|
|
|
'- Chaque fermeture du contacteur coute 50 a 150 Wh de transitoire : une session',
|
|
|
|
|
' longue et dimensionnee vaut mieux que des creneaux repetes.',
|
|
|
|
|
'On te donne des CONSTATS MESURES sur les dernieres 24 h. Ce sont des FAITS : ne les',
|
|
|
|
|
'conteste pas et ne les recalcule pas. Pour chacun, propose UNE piste : la cause la',
|
|
|
|
|
'plus probable et ce qu il faut verifier ou corriger. Tu ne pilotes rien.',
|
|
|
|
|
'',
|
|
|
|
|
'LA MAISON, telle qu elle consomme vraiment :',
|
|
|
|
|
'- Quatre personnes. Les gros appels sont BREFS et cales sur les repas :',
|
|
|
|
|
' bouilloire vers 05:45, grille-pain vers 08:00, cuisson du soir entre 20 h et 23 h.',
|
|
|
|
|
"- L heure du diner varie de TROIS HEURES d un jour a l autre : un profil horaire",
|
|
|
|
|
' moyen ne represente ni l un ni l autre.',
|
|
|
|
|
'REGLES ABSOLUES :',
|
|
|
|
|
'- N ecris AUCUN chiffre qui ne figure pas dans le constat ou son contexte.',
|
|
|
|
|
'- Si tu ne vois pas de cause, dis « cause non identifiable avec ces donnees ».',
|
|
|
|
|
'- Une piste fait au plus 280 caracteres, en francais, sans jargon inutile.',
|
|
|
|
|
'- Le contexte est la raison publiee par le controleur au debut de l episode.',
|
|
|
|
|
'',
|
|
|
|
|
'CE QUI REMPLACE LES REGLES DE SEUIL :',
|
|
|
|
|
'un bilan energetique tourne toutes les 5 minutes et te donne, pour l instant present,',
|
|
|
|
|
'l heure de croisement ou la production repassera au-dessus de la consommation,',
|
|
|
|
|
'l energie que la batterie doit couvrir d ici la, et le SOC qui serait necessaire.',
|
|
|
|
|
'RAISONNE SUR CES CHIFFRES. Il n existe plus de palier du type',
|
|
|
|
|
'"prevision forte => cible basse" : cette regle a ete mesuree fausse sur cette maison.',
|
|
|
|
|
'Si le bilan est indisponible ou perime, dis-le dans ta raison et reste prudent.',
|
|
|
|
|
'',
|
|
|
|
|
'CE QUE TU DOIS SAVOIR ET QUE PERSONNE NE TE DIRA AUTREMENT :',
|
|
|
|
|
'- SANS VEHICULE BRANCHE IL Y A QUAND MEME QUELQUE CHOSE A DECIDER : c est la MAISON',
|
|
|
|
|
' qui doit traverser la nuit. Une batterie au plancher pendant les heures creuses,',
|
|
|
|
|
' alors que le bilan reclame davantage, est une occasion manquee qui se paiera au',
|
|
|
|
|
' plein tarif le matin.',
|
|
|
|
|
'- Les chargeurs solaires declarent eux-memes s ils se BRIDENT. Si certains se',
|
|
|
|
|
' brident, il reste de la ressource REFUSEE : lui donner un preneur (voiture,',
|
|
|
|
|
' batterie) ne coute rien. S ils sont tous a fond, la production mesuree EST la',
|
|
|
|
|
' ressource.',
|
|
|
|
|
'- L hiver la nuit a couvrir est plus longue de deux heures en temps solaire : la',
|
|
|
|
|
' cible du matin y monte naturellement bien plus haut qu en ete.',
|
|
|
|
|
'',
|
|
|
|
|
'DEUX REGLES QUI PRIMENT SUR TOUT LE RESTE :',
|
|
|
|
|
'1. EXCEDENT SOLAIRE. Charger un vehicule ne se decide pas sur la production',
|
|
|
|
|
' mais sur ce qui RESTE une fois la maison servie. Cet excedent t est donne',
|
|
|
|
|
' plus bas. S il est NEGATIF, charger la voiture revient a vider la batterie',
|
|
|
|
|
' solaire : ne le fais pas, quelle que soit la prevision. S il est nul ou a',
|
|
|
|
|
' peine positif ALORS QUE DES CHARGEURS SE BRIDENT, la ressource existe mais',
|
|
|
|
|
' est refusee : la charger est gratuit.',
|
|
|
|
|
'2. LIMITE DU PARC. La marge de decharge restante t est donnee plus bas. Si la',
|
|
|
|
|
' batterie depasse 80 % de ce que le parc declare accepter, c est un sujet de',
|
|
|
|
|
' PROTECTION DU MATERIEL, pas de confort : il faut appeler le reseau meme si',
|
|
|
|
|
' le SOC parait confortable et meme hors heures creuses. Le proprietaire a un',
|
|
|
|
|
' abonnement 12 kVA et prefere payer quelques kWh plutot que fragiliser le parc.',
|
|
|
|
|
'',
|
|
|
|
|
// <<< CARTE GENEREE — ne pas editer a la main, voir tools/carte-decisions.mjs
|
|
|
|
|
'QUI ECRIT SUR QUOI (carte generee depuis les sources) :',
|
|
|
|
|
'- Contacteur reseau EDF : proprietaire = Battery / Grid Hysteresis Fix V1, AUTRES ECRIVAINS : EVCS Surplus Controller V2.6, automations, scripts',
|
|
|
|
|
'- Courant de la borne : proprietaire = EVCS Surplus Controller V2.6, AUTRES ECRIVAINS : scripts',
|
|
|
|
|
'- Verrou hysteresis : proprietaire = Battery / Grid Hysteresis Fix V1, AUTRES ECRIVAINS : scripts',
|
|
|
|
|
'SEUILS DE SOC FIGES dans le code : 20, 22, 25, 35, 40, 60, 80, 85, 90, 95, 100 % — chacun peut contredire le bilan.',
|
|
|
|
|
'Quand un actionneur bascule de facon incoherente, le coupable le plus probable',
|
|
|
|
|
'est un ecrivain AUTRE QUE SON PROPRIETAIRE. Nomme-le.',
|
|
|
|
|
// CARTE GENEREE >>>
|
|
|
|
|
'',
|
|
|
|
|
'TU NE PILOTES RIEN ET TU NE CHOISIS AUCUNE STRATEGIE.',
|
|
|
|
|
'La planification energetique est faite par le bilan, qui est calcule et mesure.',
|
|
|
|
|
'Ton seul travail est de REGARDER LE SYSTEME SE COMPORTER et de dire ce qui cloche.',
|
|
|
|
|
'',
|
|
|
|
|
'Reponds UNIQUEMENT avec un objet JSON valide, sans texte autour, au format EXACT :',
|
|
|
|
|
'{"constat":"ce que tu observes, factuel, max 200 caracteres",',
|
|
|
|
|
'"gravite":"aucune|faible|moyenne|haute",',
|
|
|
|
|
'"ce_qui_cloche":"la contradiction ou l incoherence, ou \'\' si rien",',
|
|
|
|
|
'"piste":"ce qu il faudrait verifier ou corriger, ou \'\' si rien",',
|
|
|
|
|
'"confidence":<0.0-1.0>}',
|
|
|
|
|
'',
|
|
|
|
|
'ECHELLE DE GRAVITE, ancree sur des cas reels de cette installation :',
|
|
|
|
|
'- aucune : tout est coherent. C EST LE CAS LE PLUS FREQUENT ET DE LOIN.',
|
|
|
|
|
' N invente pas un probleme pour justifier une reponse.',
|
|
|
|
|
'- faible : un ecart sans consequence mesurable — une valeur un peu hors cible,',
|
|
|
|
|
' un etat transitoire qui se resorbe seul.',
|
|
|
|
|
'- moyenne : quelque chose coute de l energie ou de l argent sans danger materiel.',
|
|
|
|
|
' Exemple reel : la consigne reseau laissee haute en plein soleil, qui',
|
|
|
|
|
' evince la production — 5,6 kW de solaire remplaces par du reseau.',
|
|
|
|
|
'- haute : un actionneur bascule de facon repetee, OU le materiel est sollicite',
|
|
|
|
|
' pres de ses limites, OU un automate defait ce qu un autre vient de',
|
|
|
|
|
' faire. Deux exemples reels : le contacteur reseau qui bascule six',
|
|
|
|
|
' fois en dix minutes le 12/09 — chaque bascule coute 50 a 150 Wh et',
|
|
|
|
|
' use le relais, et aucun automate ne le voyait ; et le 05/09 a 20:40,',
|
|
|
|
|
' le parc a -97,4 A pour une limite declaree de 100 A, soit 3 A de',
|
|
|
|
|
' marge, pendant que le reseau restait disponible et inutilise.',
|
|
|
|
|
'UNE OSCILLATION EST TOUJOURS DE GRAVITE HAUTE, meme si chaque bascule prise',
|
|
|
|
|
'isolement parait anodine : c est la repetition qui fait le degat, et c est',
|
|
|
|
|
'precisement ce qu aucune regle locale ne peut voir.',
|
|
|
|
|
'Reponds UNIQUEMENT en JSON : {"pistes":[{"code":"<code du constat>","piste":"<texte>"}]}',
|
|
|
|
|
].join('\n');
|
|
|
|
|
|
|
|
|
|
// Les etats du controleur sont des identifiants internes. Le conseiller n'a
|
|
|
|
|
// aucune raison de savoir que `day_vehicle_full_idle` veut dire « le vehicule
|
|
|
|
|
// est plein, il n'absorbera plus rien » — et sans le savoir il signale comme
|
|
|
|
|
// une anomalie un systeme qui a raison.
|
|
|
|
|
const ETATS = {
|
|
|
|
|
vehicle_not_connected: 'aucun vehicule branche',
|
|
|
|
|
idle_wait_conditions: 'a l arret, conditions de demarrage non reunies',
|
|
|
|
|
idle_wait_source: 'a l arret, en attente d une source',
|
|
|
|
|
day_vehicle_full_idle: 'vehicule PLEIN, il n absorbera plus rien (memoire du jour)',
|
|
|
|
|
night_recovery_done_idle: 'charge de nuit terminee ET batterie a la cible',
|
|
|
|
|
night_session_done_idle: 'charge de nuit terminee, batterie encore sous la cible',
|
|
|
|
|
departure_done_idle: 'charge avant depart terminee',
|
|
|
|
|
vehicle_full_or_paused: 'le vehicule ne demande plus de courant',
|
|
|
|
|
start_min_current: 'demarrage au courant minimal',
|
|
|
|
|
start_grid_hc_min_current: 'demarrage sur le reseau en heures creuses',
|
|
|
|
|
start_manual_local_min_current: 'demarrage manuel au courant minimal',
|
|
|
|
|
charging_hold_grid: 'en charge sur le reseau, consigne tenue',
|
|
|
|
|
charging_hold_manual_local: 'en charge, consigne manuelle',
|
|
|
|
|
charging_hold_cloud_tolerance: 'en charge, tolerance nuage en cours',
|
|
|
|
|
charging_step_up_battery: 'en charge, courant en hausse sur le solaire',
|
|
|
|
|
charging_step_up_grid_hc: 'en charge, courant en hausse sur le reseau',
|
|
|
|
|
charging_step_down_battery: 'en charge, courant en baisse',
|
|
|
|
|
charging_step_down_system_cap: 'en charge, courant ramene au plafond du systeme',
|
|
|
|
|
battery_current_hard_stop: 'ARRET : la batterie se dechargeait trop fort',
|
|
|
|
|
stop_conditions_lost: 'ARRET : conditions de maintien perdues',
|
|
|
|
|
stop_source_lost: 'ARRET : la source a disparu',
|
|
|
|
|
hold_min_run: 'maintenu le temps de la duree minimale de marche',
|
|
|
|
|
};
|
|
|
|
|
const etatLisible = (e) => ETATS[e] ? `${ETATS[e]} (${e})` : e;
|
|
|
|
|
|
|
|
|
|
function nombre(v, unite, defaut) {
|
|
|
|
|
return Number.isFinite(v) ? `${v}${unite}` : (defaut || 'inconnu');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const bilanLignes = ctx.balanceStatus === 'ok'
|
|
|
|
|
? [
|
|
|
|
|
`- heure de croisement production/consommation : ${ctx.balanceCrossingHour || 'inconnue'}`,
|
|
|
|
|
`- energie que la batterie doit couvrir d ici la : ${nombre(ctx.balanceEnergyToCoverKwh, ' kWh')}`,
|
|
|
|
|
`- SOC qui serait necessaire : ${nombre(ctx.balanceRequiredSoc, ' %')} (prudent : ${nombre(ctx.balanceRequiredSocPrudent, ' %')})`,
|
|
|
|
|
`- reseau juge necessaire : ${ctx.balanceGridNeeded ? 'OUI' : 'non'}${Number.isFinite(ctx.balanceGridEnergyNeededKwh) ? ` (${ctx.balanceGridEnergyNeededKwh} kWh)` : ''}`,
|
|
|
|
|
`- production actuellement bridee : ${ctx.balancePvCensored ? `OUI (${ctx.balanceCensorReason})` : 'non'}`,
|
|
|
|
|
`- manque de stockage identifie : ${nombre(ctx.balanceStorageShortfallKwh, ' kWh')}`,
|
|
|
|
|
`- prochaine traversee deficitaire : ${nombre(ctx.balanceNextDeficitKwh, ' kWh')} ${ctx.balanceNextDeficitWindow || ''}`,
|
|
|
|
|
`- vigilance orage : ${ctx.stormLevel}${Number.isFinite(ctx.stormReserveSoc) ? `, reserve conseillee ${ctx.stormReserveSoc} %` : ''}`,
|
|
|
|
|
`- calcule a ${ctx.balanceLastRun || 'heure inconnue'}`,
|
|
|
|
|
]
|
|
|
|
|
: [`- INDISPONIBLE (statut ${ctx.balanceStatus}) : raisonne sans lui, et dis-le.`];
|
|
|
|
|
|
|
|
|
|
const userPrompt = [
|
|
|
|
|
`Nous sommes ${nowParis} (heure de Paris).`,
|
|
|
|
|
`Revue du ${jourParis}. ${aExaminer.length} constat(s) a examiner :`,
|
|
|
|
|
'',
|
|
|
|
|
'ETAT MESURE :',
|
|
|
|
|
`- Batterie : SOC ${ctx.soc} %, courant ${ctx.batteryCurrentA} A (positif = charge)`,
|
|
|
|
|
`- PV : ${Math.round(ctx.pvW)} W instantane, ${Math.round(ctx.pvAvg10mW)} W sur 10 min`,
|
|
|
|
|
`- Chargeurs solaires : ${ctx.mppt.aFond}/${ctx.mppt.total} a pleine puissance, ${ctx.mppt.brides} qui se BRIDENT`,
|
|
|
|
|
`- Maison : ${Math.round(ctx.houseW)} W, dont voiture ${ctx.evcsCharging ? Math.round(ctx.evcsCurrentA * 230) : 0} W`,
|
|
|
|
|
`- EXCEDENT SOLAIRE (production moins maison hors voiture) : ${excedentSolaireW > 0 ? `+${excedentSolaireW} W, il RESTE du solaire` : excedentSolaireW < 0 ? `${excedentSolaireW} W` : '0 W, exactement a l equilibre'}${ctx.mppt.brides > 0 ? ` — ATTENTION, ce chiffre est CENSURE : ${ctx.mppt.brides} chargeurs se retiennent, la ressource reelle est PLUS HAUTE${excedentSolaireW < 0 ? ' et ce chiffre negatif ne prouve PAS qu il manque du soleil' : ''}` : excedentSolaireW < 0 ? ', DEFICIT reel : la batterie porte deja la maison' : ''}`,
|
|
|
|
|
`- MARGE DE DECHARGE du parc : ${margeDechargeA === null ? 'inconnue' : `${margeDechargeA} A restants`}${partDuPlafond === null ? '' : ` — la batterie est a ${partDuPlafond} % de son plafond`}`,
|
|
|
|
|
`- Reseau EDF : ${ctx.gridOn ? 'PRESENT, il peut alimenter la maison' : 'ABSENT, la maison vit sur batterie et solaire'} (contacteur ${ctx.gridOn ? 'ferme' : 'ouvert'}, AC-IN ${ctx.acinAvailable ? 'present' : 'absent'})`,
|
|
|
|
|
`- Consigne de puissance reseau : ${nombre(ctx.gridSetpointW, ' W')}${margeRechargeW === null ? '' : batterieDoitMonter === false && margeRechargeW > 0 ? ` — la batterie n a PAS besoin de monter (SOC ${ctx.soc} %, bilan ${ctx.balanceRequiredSoc} %), donc ces ${margeRechargeW} W de demande reseau n ont aucune destination : ils ne peuvent que remplacer du solaire` : margeRechargeW > 0 ? ` — il reste ${margeRechargeW} W pour RECHARGER la batterie, qui en a besoin` : ' — une fois la maison servie il ne reste RIEN pour recharger la batterie'}`,
|
|
|
|
|
`- Limites declarees par le parc : charge ${nombre(ctx.bmsMaxChargeA, ' A')}, decharge ${nombre(ctx.bmsMaxDischargeA, ' A')}`,
|
|
|
|
|
`- Vehicule : ${ctx.vehicleConnected ? `BRANCHE (${ctx.vehicleSelected})` : 'AUCUN branche'}${ctx.evcsCharging ? `, en charge a ${ctx.evcsCurrentA} A` : ''}`,
|
|
|
|
|
`- Controleur : ${etatLisible(ctx.surplusState)} | source ${ctx.sourceMode} | strategie reelle ${ctx.userStrategy}`,
|
|
|
|
|
`- Cible SOC matin actuellement appliquee : ${ctx.nightTargetActual} %`,
|
|
|
|
|
`- Prevision : ${ctx.todayRemainingKwh} kWh restant aujourd hui, ${ctx.tomorrowKwh} kWh demain`,
|
|
|
|
|
...aExaminer.map((c, i) => `${i + 1}. code=${c.code}\n constat : ${c.texte}\n contexte : ${String(c.contexte || '(aucun)').slice(0, 400)}`),
|
|
|
|
|
'',
|
|
|
|
|
'BILAN ENERGETIQUE :',
|
|
|
|
|
...bilanLignes,
|
|
|
|
|
'',
|
|
|
|
|
'DERNIERS EVENEMENTS DU SYSTEME (le plus recent en premier) :',
|
|
|
|
|
...(ctx.evenements.length ? ctx.evenements : ['- aucun evenement transmis']),
|
|
|
|
|
'',
|
|
|
|
|
'Quelle est ta recommandation ? JSON uniquement.',
|
|
|
|
|
'JSON uniquement.',
|
|
|
|
|
].join('\n');
|
|
|
|
|
|
|
|
|
|
return [{
|
|
|
|
|
json: {
|
|
|
|
|
enabled: ctx.advisorEnabled,
|
|
|
|
|
enabled: ctx.advisorEnabled && revueDuJour && !dejaRepondu && aExaminer.length > 0 && heureParis < 10,
|
|
|
|
|
messages: [
|
|
|
|
|
{ role: 'system', content: systemPrompt },
|
|
|
|
|
{ role: 'user', content: userPrompt },
|
|
|
|
|
],
|
|
|
|
|
context: ctx,
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
}];
|
|
|
|
|
|