Prevision de nuit : « c'est juste » quand seul le profil habituel passe (incident 28/09 21:16)
Trois verdicts au lieu de deux ; « la batterie suffit » exige le requis PRUDENT. Attribut soc_requis_maintenant_pct (l'ancien nom se lisait comme un SOC du matin). Tests 73/73, mutation qui mord. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011uJ47NxgfpGnuuMr3f2n7Z
This commit is contained in:
16
HANDOFF.md
16
HANDOFF.md
@@ -1037,5 +1037,21 @@ fermer le contacteur a midi n'y gagnerait rien et etoufferait le solaire.
|
|||||||
`input_datetime.victron_evcs_vehicle_level_at`, que le gabarit lit pour la validite.
|
`input_datetime.victron_evcs_vehicle_level_at`, que le gabarit lit pour la validite.
|
||||||
Teste en reel : 12 -> 12, reference recalee, date 14:08:41.
|
Teste en reel : 12 -> 12, reference recalee, date 14:08:41.
|
||||||
|
|
||||||
|
### 28/09 21:16 — « La batterie suffit, sans EDF » avec 0,5 point de marge : prevision de nuit corrigee
|
||||||
|
Reponse de l'agent contestee par le proprietaire (deux erreurs). (1) Mon capteur
|
||||||
|
`sensor.evcs_prevision_nuit` disait « la batterie suffit, sans EDF » : SOC 58 pour un
|
||||||
|
requis 57,5 (profil HABITUEL p50), alors que le prudent demandait 68,5 et que la soiree
|
||||||
|
consommait ~5 points/h. Ce que fait vraiment le controleur : la decision est REPRISE a
|
||||||
|
01:24 (session si SOC < exigence recalculee a ce moment-la) -> estimation a la main ~42 %
|
||||||
|
pour ~39 % requis au profil habituel, EDF probable si la soiree reste forte. (2) Cote
|
||||||
|
SPARK : l'agent avait lu `soc_requis_au_retour_solaire_pct` comme un SOC PROJETE du matin.
|
||||||
|
Correctif (tests diagnostics 69 -> 73, mutation qui mord) : TROIS verdicts (`verdict`) —
|
||||||
|
`edf_engage` (session engagee), `reseau_necessaire`, `batterie_suffit` (SOC >= requis
|
||||||
|
PRUDENT seulement), `juste` (sinon : « c'est juste … le systeme refait le calcul a
|
||||||
|
01:24 : si la soiree a consomme davantage, il prendra EDF en heures creuses »).
|
||||||
|
Attribut renomme `soc_requis_maintenant_pct` (+ `soc_requis_prudent_maintenant_pct`,
|
||||||
|
`marge_points`, `debut_heures_creuses`) ; aucun SOC du matin n'est publie. Verifie en
|
||||||
|
reel 21:27 : `juste`, marge 0,5.
|
||||||
|
|
||||||
---
|
---
|
||||||
*Document mis a jour le : 28/09/2026 (~02:15) — VRM remplace Forecast.Solar (comparaison le 05/10), tenir jusqu'a la prochaine HC, prevision de nuit pour l'agent*
|
*Document mis a jour le : 28/09/2026 (~02:15) — VRM remplace Forecast.Solar (comparaison le 05/10), tenir jusqu'a la prochaine HC, prevision de nuit pour l'agent*
|
||||||
|
|||||||
@@ -307,8 +307,37 @@ for (const [nom, r, test] of imp) {
|
|||||||
(r) => !/73\.7|26\.95/.test(JSON.stringify(r))],
|
(r) => !/73\.7|26\.95/.test(JSON.stringify(r))],
|
||||||
['bilan perime (> 30 min) -> indisponible', previsionNuit(base(), new Date('2026-09-28T00:30:00Z')),
|
['bilan perime (> 30 min) -> indisponible', previsionNuit(base(), new Date('2026-09-28T00:30:00Z')),
|
||||||
(r) => r.etat === 'indisponible'],
|
(r) => r.etat === 'indisponible'],
|
||||||
['reseau non necessaire -> 0 et « la batterie suffit »', (() => { const m = base(); m.set('input_boolean.evcs_balance_grid_needed', { state: 'off' }); return previsionNuit(m, t); })(),
|
['session engagee -> « EDF est branche pour la nuit », jamais « la batterie suffit »', previsionNuit(base(), t),
|
||||||
(r) => r.etat === 0 && /batterie suffit/.test(r.attributs.explication)],
|
(r) => r.attributs.verdict === 'edf_engage' && /EDF est branche pour la nuit/.test(r.attributs.explication)
|
||||||
|
&& !/batterie suffit/.test(r.attributs.explication)],
|
||||||
|
// Releve REEL du 28/09 21:15 (reponse contestee par le proprietaire) :
|
||||||
|
// requis 57,5 %, SOC 58, prudent 68,5, pas de session. « La batterie suffit,
|
||||||
|
// sans EDF » etait faux : 0,5 point de marge sur le profil habituel.
|
||||||
|
['28/09 21:15 : marge 0,5 point, prudent 68,5 > SOC 58 -> « c est juste », decision reprise a 01:24', (() => {
|
||||||
|
const m = base(); for (const [k, v] of Object.entries({
|
||||||
|
'input_boolean.evcs_balance_grid_needed': 'off', 'input_number.evcs_balance_required_soc': '57.5',
|
||||||
|
'input_number.evcs_balance_required_soc_prudent': '68.5', 'sensor.victron_battery_soc': '58',
|
||||||
|
'input_boolean.victron_evcs_night_grid_session_committed': 'off', 'sensor.evcs_reseau': 'non_raccorde',
|
||||||
|
'input_number.evcs_balance_energy_to_cover_kwh': '10.64', 'input_datetime.victron_evcs_hc_night_start': '01:24:00',
|
||||||
|
})) m.set(k, { state: v }); return previsionNuit(m, t); })(),
|
||||||
|
(r) => r.etat === 0 && r.attributs.verdict === 'juste' && r.attributs.marge_points === 0.5
|
||||||
|
&& /C est juste/.test(r.attributs.explication) && /a 01:24/.test(r.attributs.explication)
|
||||||
|
&& !/sans EDF/.test(r.attributs.explication) && !/batterie suffit jusqu/.test(r.attributs.explication)],
|
||||||
|
['SOC au-dessus du requis PRUDENT -> « la batterie suffit »', (() => {
|
||||||
|
const m = base(); for (const [k, v] of Object.entries({
|
||||||
|
'input_boolean.evcs_balance_grid_needed': 'off', 'input_number.evcs_balance_required_soc': '40',
|
||||||
|
'input_number.evcs_balance_required_soc_prudent': '55', 'sensor.victron_battery_soc': '70',
|
||||||
|
'input_boolean.victron_evcs_night_grid_session_committed': 'off',
|
||||||
|
})) m.set(k, { state: v }); return previsionNuit(m, t); })(),
|
||||||
|
(r) => r.attributs.verdict === 'batterie_suffit' && /batterie suffit jusqu au retour du solaire/.test(r.attributs.explication)],
|
||||||
|
['prudent illisible -> jamais « la batterie suffit », mais « c est juste »', (() => {
|
||||||
|
const m = base(); for (const [k, v] of Object.entries({
|
||||||
|
'input_boolean.evcs_balance_grid_needed': 'off', 'input_number.evcs_balance_required_soc': '40',
|
||||||
|
'sensor.victron_battery_soc': '70', 'input_boolean.victron_evcs_night_grid_session_committed': 'off',
|
||||||
|
})) m.set(k, { state: v }); return previsionNuit(m, t); })(),
|
||||||
|
(r) => r.attributs.verdict === 'juste'],
|
||||||
|
['jamais d attribut qui ressemble a un SOC du matin (incident 21:16)', previsionNuit(base(), t),
|
||||||
|
(r) => !('soc_requis_au_retour_solaire_pct' in r.attributs) && 'soc_requis_maintenant_pct' in r.attributs],
|
||||||
];
|
];
|
||||||
for (const [nom, r, test] of pn) {
|
for (const [nom, r, test] of pn) {
|
||||||
const ok = test(r); if (!ok) { ko++; console.log(JSON.stringify(r)); }
|
const ok = test(r); if (!ok) { ko++; console.log(JSON.stringify(r)); }
|
||||||
|
|||||||
@@ -583,16 +583,34 @@ export function previsionNuit(st, maintenant = new Date()) {
|
|||||||
const reseauKwh = nombre(st, 'input_number.evcs_balance_grid_energy_needed_kwh');
|
const reseauKwh = nombre(st, 'input_number.evcs_balance_grid_energy_needed_kwh');
|
||||||
const reseauNecessaire = txt('input_boolean.evcs_balance_grid_needed') === 'on';
|
const reseauNecessaire = txt('input_boolean.evcs_balance_grid_needed') === 'on';
|
||||||
const retour = txt('input_text.evcs_balance_crossing_hour');
|
const retour = txt('input_text.evcs_balance_crossing_hour');
|
||||||
|
// ⚠️ SOC REQUIS MAINTENANT, pas un SOC projete du matin (incident 28/09 21:16 :
|
||||||
|
// l'agent a lu 57,5 comme « la batterie arrivera a 57-58 % au retour du
|
||||||
|
// soleil »). Le bilan ne projette AUCUN SOC du matin ; ce capteur non plus.
|
||||||
const socRequis = nombre(st, 'input_number.evcs_balance_required_soc');
|
const socRequis = nombre(st, 'input_number.evcs_balance_required_soc');
|
||||||
|
const socPrudent = nombre(st, 'input_number.evcs_balance_required_soc_prudent');
|
||||||
const soc = nombre(st, 'sensor.victron_battery_soc');
|
const soc = nombre(st, 'sensor.victron_battery_soc');
|
||||||
const reseau = txt('sensor.evcs_reseau');
|
const reseau = txt('sensor.evcs_reseau');
|
||||||
const session = txt('input_boolean.victron_evcs_night_grid_session_committed') === 'on';
|
const session = txt('input_boolean.victron_evcs_night_grid_session_committed') === 'on';
|
||||||
const branche = txt('binary_sensor.victron_evcs_vehicle_connected');
|
const branche = txt('binary_sensor.victron_evcs_vehicle_connected');
|
||||||
|
const hcDebut = (txt('input_datetime.victron_evcs_hc_night_start') || '').slice(0, 5) || null;
|
||||||
|
const marge = soc !== null && socRequis !== null ? Math.round((soc - socRequis) * 10) / 10 : null;
|
||||||
|
|
||||||
|
// TROIS verdicts, pas deux (incident 28/09 21:16 : « la batterie suffit, sans
|
||||||
|
// EDF » avec 0,5 point de marge sur le profil HABITUEL, alors que le profil
|
||||||
|
// prudent demandait 68,5 % pour un SOC de 58 %). Le profil p50 est une
|
||||||
|
// mediane : a zero marge, c'est pile ou face. La decision reelle est reprise
|
||||||
|
// a l'ouverture des heures creuses, sur l'exigence recalculee a ce moment-la.
|
||||||
|
const verdict = session ? 'edf_engage'
|
||||||
|
: reseauNecessaire ? 'reseau_necessaire'
|
||||||
|
: socPrudent !== null && soc !== null && soc >= socPrudent ? 'batterie_suffit'
|
||||||
|
: 'juste';
|
||||||
|
|
||||||
const attributs = {
|
const attributs = {
|
||||||
maison_kwh_a_couvrir: aCouvrir, heure_retour_solaire: retour,
|
maison_kwh_a_couvrir: aCouvrir, heure_retour_solaire: retour,
|
||||||
reseau_necessaire: reseauNecessaire, reseau_necessaire_kwh: reseauNecessaire ? reseauKwh : 0,
|
reseau_necessaire: reseauNecessaire, reseau_necessaire_kwh: reseauNecessaire ? reseauKwh : 0,
|
||||||
soc_actuel_pct: soc, soc_requis_au_retour_solaire_pct: socRequis,
|
verdict,
|
||||||
|
soc_actuel_pct: soc, soc_requis_maintenant_pct: socRequis, soc_requis_prudent_maintenant_pct: socPrudent,
|
||||||
|
marge_points: marge, debut_heures_creuses: hcDebut,
|
||||||
contacteur_edf: reseau, session_reseau_nuit_engagee: session,
|
contacteur_edf: reseau, session_reseau_nuit_engagee: session,
|
||||||
voiture: branche === 'on' ? 'branchee' : branche === 'off' ? 'aucune' : 'inconnue',
|
voiture: branche === 'on' ? 'branchee' : branche === 'off' ? 'aucune' : 'inconnue',
|
||||||
bilan_calcule_il_y_a_min: age,
|
bilan_calcule_il_y_a_min: age,
|
||||||
@@ -610,12 +628,16 @@ export function previsionNuit(st, maintenant = new Date()) {
|
|||||||
: attributs.voiture === 'branchee'
|
: attributs.voiture === 'branchee'
|
||||||
? 'Un vehicule est branche : sa charge s ajoute et sera decidee par le controleur.'
|
? 'Un vehicule est branche : sa charge s ajoute et sera decidee par le controleur.'
|
||||||
: 'Etat du vehicule inconnu.';
|
: 'Etat du vehicule inconnu.';
|
||||||
const edf = reseauNecessaire
|
const hc = hcDebut ? `a ${hcDebut}` : 'a l ouverture des heures creuses';
|
||||||
? `Environ ${reseauKwh} kWh a prendre sur EDF avant le retour du solaire.`
|
const edf = {
|
||||||
: 'La batterie suffit, sans EDF, jusqu au retour du solaire.';
|
edf_engage: `EDF est branche pour la nuit, en heures creuses${reseauNecessaire ? ` : environ ${reseauKwh} kWh a prendre sur EDF avant le retour du solaire` : ''} ; la batterie est tenue jusqu au matin.`,
|
||||||
|
reseau_necessaire: `Environ ${reseauKwh} kWh a prendre sur EDF en heures creuses avant le retour du solaire.`,
|
||||||
|
batterie_suffit: 'La batterie suffit jusqu au retour du solaire, meme si la maison consomme plus que d habitude : EDF ne devrait pas etre necessaire.',
|
||||||
|
juste: `C est juste : la batterie suffit seulement si la maison consomme comme d habitude${marge !== null ? ` (marge ${String(marge).replace('.', ',')} point${Math.abs(marge) >= 2 ? 's' : ''})` : ''}. ` +
|
||||||
|
`Le systeme refait le calcul ${hc} : si la soiree a consomme davantage, il prendra EDF en heures creuses.`,
|
||||||
|
}[verdict];
|
||||||
const texte = `La maison doit couvrir environ ${aCouvrir} kWh jusqu au retour du solaire vers ${retour}. ` +
|
const texte = `La maison doit couvrir environ ${aCouvrir} kWh jusqu au retour du solaire vers ${retour}. ` +
|
||||||
`${edf} Batterie a ${soc !== null ? Math.round(soc) : '?'} %. ${voiture}` +
|
`${edf} Batterie a ${soc !== null ? Math.round(soc) : '?'} %. ${voiture}`;
|
||||||
`${session ? ' La session reseau de la nuit est engagee.' : ''}`;
|
|
||||||
return { etat: reseauNecessaire ? reseauKwh : 0, attributs: { ...attributs, statut: 'frais', explication: texte, texte_rapport: texte } };
|
return { etat: reseauNecessaire ? reseauKwh : 0, attributs: { ...attributs, statut: 'frais', explication: texte, texte_rapport: texte } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user