Files
EVCS/src/energy-balance/BUILD_HA_CALLS_PROFILES.js
Tallagrand Thierry 6dc5ea25cb Chantier etapes 5b et 5c : recalcul quotidien des profils et fenetre de deficit suivante
5c — BALANCE ne repondait qu'a "combien jusqu'a ce que le solaire prenne le
relais". A 15 h la reponse est zero : exacte mais inutile, le deficit qui
approche etant celui de la nuit. Ajout de la prochaine traversee deficitaire
(heure de debut, energie, SOC a atteindre avant). Ce SOC n'est PAS ecrete a
100 % : au-dela, meme une batterie pleine ne passe pas, et le manque est
publie comme storage_shortfall_kwh — c'est le chiffre du dimensionnement.

5b — Workflow "EVCS Energy Profiles Daily V1" (N6UaB4ZozsbRPrFb), actif,
03:30 : lit /api/history/period sur 30 jours glissants et republie les quatre
CSV (conso p50 et p80, enveloppe de production, taux de censure).
CONSUMPTION_PROFILE devient DAILY_PROFILES et sait desormais decoder la
reponse brute de l'API en plus des series deja pretes.

Piege trouve : sans end_time, /api/history/period ne renvoie qu'UNE journee,
silencieusement. Mes premieres mesures "sur 3 jours" n'en couvraient qu'une.

Validation croisee : profils calcules depuis l'API REST (etats bruts, 11 j)
contre profils calcules depuis les statistiques long terme (28 j) — 0,20 kWh/h
d'ecart absolu moyen, par deux chemins de donnees independants.

BUILD_HA_CALLS_PROFILES refuse d'ecraser les profils en place si l'historique
ne couvre pas les 24 heures. Meme garantie de perimetre que le bilan.
Suite a 27 scenarios.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
2026-08-24 15:58:10 +02:00

63 lines
2.8 KiB
JavaScript

// EVCS Energy Balance — BUILD_HA_CALLS_PROFILES
// =====================================================================
// Publie les quatre profils recalculés dans leurs helpers input_text.
//
// MÊME GARANTIE DE PÉRIMÈTRE que le workflow de bilan : ce nœud n'écrit que
// vers des entités `evcs_balance_*`, plus une trace logbook. La suite de tests
// échoue si une écriture en sort.
//
// Refus explicite de publier un profil douteux : si l'historique est trop court
// pour couvrir les 24 heures, on n'écrase PAS les profils en place. Mieux vaut
// un profil un peu ancien qu'un profil creux.
// =====================================================================
const profiles = $input.first().json;
function stampParis() {
const p = new Intl.DateTimeFormat('fr-FR', {
timeZone: 'Europe/Paris', day: '2-digit', month: '2-digit',
hour: '2-digit', minute: '2-digit', hour12: false,
}).formatToParts(new Date());
const g = t => p.find(x => x.type === t)?.value || '';
return `${g('day')}/${g('month')} ${g('hour')}:${g('minute')}`;
}
const calls = [];
function setText(entity, value) {
calls.push({ json: {
domain: 'input_text', service: 'set_value',
data: { entity_id: entity, value: (value ?? '').toString().slice(0, 250) },
} });
}
if (!profiles.complete) {
setText('input_text.evcs_balance_status',
`profils NON mis à jour : ${profiles.quality.unreliableHours.length} h sans assez de jours observés`);
calls.push({ json: {
domain: 'logbook', service: 'log',
data: {
name: 'EVCS Bilan — profils quotidiens',
message: `Recalcul refusé : historique insuffisant (${profiles.days} jour(s), heures creuses : ${profiles.quality.unreliableHours.join(', ')}). Profils précédents conservés.`,
entity_id: 'input_text.evcs_balance_status',
},
} });
return calls;
}
setText('input_text.evcs_balance_consumption_p50', profiles.csv.p50);
setText('input_text.evcs_balance_consumption_p80', profiles.csv.p80);
setText('input_text.evcs_balance_envelope', profiles.csv.envelope);
setText('input_text.evcs_balance_censorship_rate', profiles.csv.censorshipRate);
setText('input_text.evcs_balance_last_run', stampParis());
calls.push({ json: {
domain: 'logbook', service: 'log',
data: {
name: 'EVCS Bilan — profils quotidiens',
message: `Profils recalculés sur ${profiles.days} jours (${profiles.quality.hoursUsed} h de consommation, ${profiles.quality.pvHoursUsed} h de production, ${profiles.quality.countersReset} remise(s) à zéro écartée(s)). Consommation typique ${profiles.totals.typicalDailyKwh} kWh/j, prudente ${profiles.totals.prudentDailyKwh} kWh/j. Production démontrée ${profiles.production.demonstratedDailyKwh} kWh/j, ${profiles.production.censoredDayHours} heure(s) de jour censurée(s).`,
entity_id: 'input_text.evcs_balance_consumption_p50',
},
} });
return calls;