Bilan : reserve d orage, en mode ombre
Une coupure reseau n arrete pas l installation : le solaire et la batterie continuent, seul le filet de secours disparait. La reserve a tenir est donc la couverture prudente p80 que le bilan calcule deja — aucune constante nouvelle. En deficit c est la fenetre traversee, en surplus la prochaine, car a 15 h la reponse « rien a couvrir » est exacte et inutile. Le seuil d activation est la vigilance orange, echelle officielle de Meteo-France et non un nombre choisi ici. Le snapshot lit l alerte sans connaitre la commune, donc le code marche des l installation de l integration et publie « indisponible » en attendant. Tests 38 -> 42. Le fixture du test de perimetre ombre ne contenait pas storm : les nouvelles ecritures n etaient pas couvertes. Corrige, assertion explicite, preuve par mutation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
This commit is contained in:
@@ -88,6 +88,14 @@ const SUMMARIZE = {
|
||||
nextDeficitEnergyKwh: o.nextDeficit.found ? o.nextDeficit.energyKwh : null,
|
||||
nextDeficitRequiredSocPct: o.nextDeficit.found ? o.nextDeficit.requiredSocAtStartPct : null,
|
||||
storageShortfallKwh: o.nextDeficit.found ? Math.round(o.nextDeficit.storageShortfallKwh * 100) / 100 : null,
|
||||
// Réserve d'orage : niveau lu, activation, et le chiffre qui compte —
|
||||
// ce qu'il faut en batterie pour traverser sans réseau.
|
||||
stormLevel: o.storm.level,
|
||||
stormActive: o.storm.active,
|
||||
stormReserveKwh: o.storm.reserveKwh,
|
||||
stormReserveSocPct: o.storm.reserveSocPct,
|
||||
stormCovered: o.storm.covered,
|
||||
stormShortfallKwh: o.storm.shortfallKwh,
|
||||
}) : ({ usable: false, missing: o.missing.join(',') }),
|
||||
};
|
||||
|
||||
@@ -101,6 +109,9 @@ SUMMARIZE.BUILD_HA_CALLS = (calls) => {
|
||||
outOfScope: entities.filter(e => !/^(input_text|input_number|input_boolean)\.evcs_balance_/.test(e)).join(','),
|
||||
wroteRequiredSoc: entities.includes('input_number.evcs_balance_required_soc'),
|
||||
wroteGridNeeded: entities.includes('input_boolean.evcs_balance_grid_needed'),
|
||||
// Les 5 écritures de la réserve d'orage sont-elles bien émises ET dans le
|
||||
// périmètre ombre ? (outOfScope les attraperait si elles en sortaient).
|
||||
wroteStorm: entities.filter(e => /evcs_balance_storm_/.test(e)).length,
|
||||
hasLogbook: calls.some(c => c.json.domain === 'logbook'),
|
||||
wroteProfiles: entities.includes('input_text.evcs_balance_consumption_p50'),
|
||||
status: (calls.find(c => c.json.data.entity_id === 'input_text.evcs_balance_status') || { json: { data: {} } }).json.data.value,
|
||||
|
||||
@@ -145,4 +145,113 @@ export const balanceScenarios = [
|
||||
missing: 'usable_capacity_kwh',
|
||||
},
|
||||
},
|
||||
|
||||
// ================= RÉSERVE D'ORAGE (27/08/2026) =================
|
||||
// Une coupure réseau n'arrête PAS l'installation : le solaire et la batterie
|
||||
// continuent, seul le filet de secours disparaît. La réserve à tenir est donc
|
||||
// la couverture PRUDENTE (p80) de la fenêtre de déficit qui compte — pas une
|
||||
// nouvelle constante.
|
||||
{
|
||||
node: 'BALANCE',
|
||||
id: 'orage-vigilance-absente',
|
||||
label: 'Intégration Météo-France non installée : niveau indisponible, aucune activation, mais la réserve reste chiffrée',
|
||||
snapshot: {
|
||||
nowMs: Date.parse('2026-08-24T06:43:00+02:00'),
|
||||
consumption_p50: P50,
|
||||
consumption_p80: P80,
|
||||
horizon: horizonFrom('2026-08-24T06:43:00+02:00', 18, PROD_24_08),
|
||||
soc_percent: 21,
|
||||
usable_capacity_kwh: 15,
|
||||
soc_floor_percent: 22,
|
||||
storm_alert_level: 'indisponible',
|
||||
},
|
||||
expect: {
|
||||
usable: true,
|
||||
stormLevel: 'indisponible',
|
||||
stormActive: false,
|
||||
// La réserve se calcule quand même : 2,42 kWh pour la matinée du 24/08
|
||||
// sur le profil prudent, soit SOC 38,1 %.
|
||||
stormReserveKwh: 2.42,
|
||||
stormReserveSocPct: 38.1,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: 'BALANCE',
|
||||
id: 'orage-vigilance-jaune-pas-active',
|
||||
label: 'Vigilance jaune : en dessous du seuil de Météo-France, la réserve est publiée mais pas déclarée d\'actualité',
|
||||
snapshot: {
|
||||
nowMs: Date.parse('2026-08-24T06:43:00+02:00'),
|
||||
consumption_p50: P50,
|
||||
consumption_p80: P80,
|
||||
horizon: horizonFrom('2026-08-24T06:43:00+02:00', 18, PROD_24_08),
|
||||
soc_percent: 21,
|
||||
usable_capacity_kwh: 15,
|
||||
soc_floor_percent: 22,
|
||||
storm_alert_level: 'Jaune',
|
||||
},
|
||||
expect: {
|
||||
usable: true,
|
||||
stormLevel: 'jaune',
|
||||
stormActive: false,
|
||||
stormReserveKwh: 2.42,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: 'BALANCE',
|
||||
id: 'orage-vigilance-orange-deficit-decouvert',
|
||||
label: 'Vigilance orange à 06:43 avec SOC 21 % : la matinée ne passe pas sans réseau, le manque est chiffré',
|
||||
snapshot: {
|
||||
nowMs: Date.parse('2026-08-24T06:43:00+02:00'),
|
||||
consumption_p50: P50,
|
||||
consumption_p80: P80,
|
||||
horizon: horizonFrom('2026-08-24T06:43:00+02:00', 18, PROD_24_08),
|
||||
soc_percent: 21,
|
||||
usable_capacity_kwh: 15,
|
||||
soc_floor_percent: 22,
|
||||
storm_alert_level: 'Orange',
|
||||
storm_alert_detail: { 'Orages': 'Orange', 'Vent violent': 'Jaune' },
|
||||
},
|
||||
expect: {
|
||||
usable: true,
|
||||
stormLevel: 'orange',
|
||||
stormActive: true,
|
||||
stormReserveKwh: 2.42,
|
||||
stormReserveSocPct: 38.1,
|
||||
stormCovered: false,
|
||||
// SOC 21 contre 38,1 requis : il manque 2,57 kWh pour tenir la matinée
|
||||
// sans réseau.
|
||||
stormShortfallKwh: 2.57,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: 'BALANCE',
|
||||
id: 'orage-vigilance-rouge-surplus-nuit-a-venir',
|
||||
label: 'Vigilance rouge à 15:30, batterie à 95 % : la fenêtre qui compte est la nuit qui vient, pas celle en cours',
|
||||
snapshot: {
|
||||
nowMs: Date.parse('2026-08-24T15:30:00+02:00'),
|
||||
consumption_p50: P50,
|
||||
consumption_p80: P80,
|
||||
horizon: horizonFrom('2026-08-24T15:30:00+02:00', 24, {
|
||||
...PROD_24_08,
|
||||
15: 3.00, 16: 2.80, 17: 2.60, 18: 2.40, 19: 1.20, 20: 0.20,
|
||||
7: 1.50, 8: 3.00, 9: 5.00,
|
||||
}),
|
||||
soc_percent: 95,
|
||||
usable_capacity_kwh: 15,
|
||||
soc_floor_percent: 22,
|
||||
storm_alert_level: 'Rouge',
|
||||
},
|
||||
expect: {
|
||||
usable: true,
|
||||
stormLevel: 'rouge',
|
||||
stormActive: true,
|
||||
// Bascule sur la fenêtre de 19 h : 12,77 kWh, soit un SOC requis de
|
||||
// 107,1 % — même pleine, la batterie actuelle ne passe pas la nuit sans
|
||||
// réseau. C'est le signal de dimensionnement, il ne doit pas être écrêté.
|
||||
stormReserveKwh: 12.77,
|
||||
stormReserveSocPct: 107.1,
|
||||
stormCovered: false,
|
||||
stormShortfallKwh: 1.82,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -30,6 +30,13 @@ const BALANCE_OK = {
|
||||
},
|
||||
vehicle: { surplusAvailableKwh: 20.17 },
|
||||
confidence: { deficitTrustworthy: true },
|
||||
// La réserve d'orage fait partie de la sortie normale de BALANCE : elle doit
|
||||
// figurer ici, sinon la garantie de périmètre ne couvre pas ses écritures.
|
||||
storm: {
|
||||
level: 'orange', known: true, active: true, phenomena: ['Orages'],
|
||||
windowStartHour: null, reserveKwh: 2.42, reserveSocPct: 38.1,
|
||||
socMarginPct: -17.1, covered: false, shortfallKwh: 2.57,
|
||||
},
|
||||
};
|
||||
|
||||
// Le même bilan, mais sans croisement trouvé sur l'horizon : le SOC requis
|
||||
@@ -54,6 +61,9 @@ export const shadowScenarios = [
|
||||
outOfScope: '',
|
||||
wroteRequiredSoc: true,
|
||||
wroteGridNeeded: true,
|
||||
// Les 5 écritures de la réserve d'orage sont émises et restent dans le
|
||||
// périmètre ombre.
|
||||
wroteStorm: 5,
|
||||
hasLogbook: true,
|
||||
status: 'ok',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user