From 75252d64bed3813bf4fb0caa175bc34bb19391ba Mon Sep 17 00:00:00 2001 From: Tallagrand Thierry Date: Sat, 10 Oct 2026 00:31:59 +0200 Subject: [PATCH] Arbitre : voiture qui ne charge pas avec EDF ferme (tout etat), alerte recharge bridee --- HANDOFF.md | 12 +++++++++++- dashboards/evcs_master.yaml | 2 +- tests/arbitrage/run.mjs | 14 ++++++++++++++ tools/arbitrage.mjs | 34 +++++++++++++++++++++++++++++----- tools/historise-evcs.mjs | 15 +++++++++++---- 5 files changed, 66 insertions(+), 11 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index 3523bfa..368ce95 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -2301,5 +2301,15 @@ batterie a 32 A et JEEP (branchee 13:01, 52 %) qui ne demarre pas. SOC 47 % a 12 SPARK previent par WhatsApp. - ⚠️ `switch.deshumide_sdb_socket_1` `unavailable` depuis 00:25 (prise debranchee ?). +- SPARK : relais des alertes de piles en place (un message groupe des alertes NOUVELLES, cle entite|statut, envois + 08:00-20:59 ; les 9 premieres partent vers 08:00). +- **Arbitre, deux cas de plus (accord du proprietaire)** (`tools/arbitrage.mjs`, tests 18/18) : (1) cas + `voiture_bloquee_edf_ferme` ELARGI a toute voiture branchee non pleine qui ne charge pas avec EDF ferme en HC, quel + que soit l'etat du controleur (le 09/10 midi la JEEP etait en « Reseau HC autorise » / attente) ; levier : mode de + nuit « complete » si la nuit en sans_edf, sinon `force_grid_now` (expire en fin d'HC). (2) **ALERTE** + `recharge_bridee` (sans choix, etat `alerte` de `sensor.evcs_arbitrage`) : session HC, manque > 0,5 kWh, SOC < 95, + batterie < 50 % du courant ACM pendant 10 min (09/10 midi : 32 A pour 140 A). Carte : « Alerte envoyee par + WhatsApp (rien a repondre) ». Service redemarre ~00:45. SPARK : envoyer l'alerte une fois par id, aucune reponse. + --- -*Document mis a jour le : 10/10/2026 (~00:40) — conseiller relecteur de la journee (revue calculee + pistes) ; nuit : voiture qui profite d'EDF ferme pour la batterie, nuit qui prepare le soir les jours annonces faibles ; 08/10/2026 (~00:15) — jour faible : charge maximale en HC de midi (bilan 100 %, ACM plafond + solaire) ; 07/10/2026 (~22:45) — reprise de 16 h verifiee, EDF en HP a 18:03, bilan : reprise du soir et prevision recalee sur la matinee ; bilan HC de midi, reprise du soir apres coupure en pleine chauffe, mesure ECS sur 10 min ; recharge reseau HC : courant calcule (12 kVA) et consigne non tenue ; HC de midi : AUTO ne chauffe pas (BOOST), alerte v2, Jeep (contact, relecture, zone), recharge batterie bridee a 30 A par l'ACM ; jour faible = EDF les deux fenetres HC pour le chauffe-eau, alerte BOOST, Victron Mobile refait ; 1re decision jour faible en reel, mesure ECS reparee, douches 06:00/20:00, Grafana chauffe-eau, tableaux ECS ; chauffe-eau en HC de nuit les jours faibles (decision du bilan, workflow ECS sous controle du depot, VRM, V2.3 desactive) ; garde batterie reseau : voiture bridee a 6 A au lieu d'un arret ; garde-fou concierge 2 h ; allers-retours brefs de la borne retrouves dans l'historique HA (echange VW -> JEEP de 20 s), production 14,5 kWh contre 22 prevus, defaut garde batterie en mode reseau a trancher ; voiture a midi : la batterie n'est plus comptee comme du solaire (EDF automatique les jours de depart si le surplus ne couvre pas) ; nuit 05->06 : JEEP chargee sur EDF en complete (6,32 kWh), budget 0 du a la prevision, Windy/ECMWF contre VRM, a comparer ce soir ; concierge aligne sur la preuve fin de trajet ; a verifier : nuit 05->06 et mardi faible, comparaison VRM/Forecast.Solar ; JEEP identifiee aussi par sa fin de trajet, prevision de la nuit 05->06 ; identification JEEP reussie en reel, correction humaine qui prime (refus supprime), session du 04/10 coupee ; EVCS Master en quatre onglets ; Jeep lue par Uconnect et identifiee seule, capacite JEEP 10 kWh, concierge variante (a) ; releve par photo (SPARK), hass-uconnect en attente ; ecart semaine/week-end mesure (pas de profil week-end la nuit) ; nuit du budget : budget borne au plancher prudent, sans EDF = voiture bloquee meme EDF ferme (batterie seule) ; Energie : sauts corriges ; tableau Energie complete (Sud/Ouest, batterie par integration, borne ; aucun redemarrage HA) ; kilometrage des vehicules (km/kWh, capacite, releves de reference, agent cable 15/15) ; a verifier le 04/10 au matin : nuit du budget JEEP ; concierge : vehicule non confirme demande, heure de saisie ; Grafana vehicules en tableau ; Grafana vehicules : ZERO et Invite ajoutes, session 16:46 reetiquetee ZERO ; prevision SOC 10 h ~35 % ; concierge fiable : niveau note par le script, une ligne, dedoublonnage, simulation par drapeau ; agent : message incompris = question, bourse sur demande explicite (banc final 6/6) ; a verifier : nuit du samedi 03 au dimanche 04/10 (budget < besoin JEEP), cron orange-cloture lundi 05/10 apres 17:40, comparaison VRM / Forecast.Solar lundi 05/10* +*Document mis a jour le : 10/10/2026 (~00:50) — conseiller relecteur de la journee (revue calculee + pistes) ; nuit : voiture qui profite d'EDF ferme pour la batterie, nuit qui prepare le soir les jours annonces faibles ; 08/10/2026 (~00:15) — jour faible : charge maximale en HC de midi (bilan 100 %, ACM plafond + solaire) ; 07/10/2026 (~22:45) — reprise de 16 h verifiee, EDF en HP a 18:03, bilan : reprise du soir et prevision recalee sur la matinee ; bilan HC de midi, reprise du soir apres coupure en pleine chauffe, mesure ECS sur 10 min ; recharge reseau HC : courant calcule (12 kVA) et consigne non tenue ; HC de midi : AUTO ne chauffe pas (BOOST), alerte v2, Jeep (contact, relecture, zone), recharge batterie bridee a 30 A par l'ACM ; jour faible = EDF les deux fenetres HC pour le chauffe-eau, alerte BOOST, Victron Mobile refait ; 1re decision jour faible en reel, mesure ECS reparee, douches 06:00/20:00, Grafana chauffe-eau, tableaux ECS ; chauffe-eau en HC de nuit les jours faibles (decision du bilan, workflow ECS sous controle du depot, VRM, V2.3 desactive) ; garde batterie reseau : voiture bridee a 6 A au lieu d'un arret ; garde-fou concierge 2 h ; allers-retours brefs de la borne retrouves dans l'historique HA (echange VW -> JEEP de 20 s), production 14,5 kWh contre 22 prevus, defaut garde batterie en mode reseau a trancher ; voiture a midi : la batterie n'est plus comptee comme du solaire (EDF automatique les jours de depart si le surplus ne couvre pas) ; nuit 05->06 : JEEP chargee sur EDF en complete (6,32 kWh), budget 0 du a la prevision, Windy/ECMWF contre VRM, a comparer ce soir ; concierge aligne sur la preuve fin de trajet ; a verifier : nuit 05->06 et mardi faible, comparaison VRM/Forecast.Solar ; JEEP identifiee aussi par sa fin de trajet, prevision de la nuit 05->06 ; identification JEEP reussie en reel, correction humaine qui prime (refus supprime), session du 04/10 coupee ; EVCS Master en quatre onglets ; Jeep lue par Uconnect et identifiee seule, capacite JEEP 10 kWh, concierge variante (a) ; releve par photo (SPARK), hass-uconnect en attente ; ecart semaine/week-end mesure (pas de profil week-end la nuit) ; nuit du budget : budget borne au plancher prudent, sans EDF = voiture bloquee meme EDF ferme (batterie seule) ; Energie : sauts corriges ; tableau Energie complete (Sud/Ouest, batterie par integration, borne ; aucun redemarrage HA) ; kilometrage des vehicules (km/kWh, capacite, releves de reference, agent cable 15/15) ; a verifier le 04/10 au matin : nuit du budget JEEP ; concierge : vehicule non confirme demande, heure de saisie ; Grafana vehicules en tableau ; Grafana vehicules : ZERO et Invite ajoutes, session 16:46 reetiquetee ZERO ; prevision SOC 10 h ~35 % ; concierge fiable : niveau note par le script, une ligne, dedoublonnage, simulation par drapeau ; agent : message incompris = question, bourse sur demande explicite (banc final 6/6) ; a verifier : nuit du samedi 03 au dimanche 04/10 (budget < besoin JEEP), cron orange-cloture lundi 05/10 apres 17:40, comparaison VRM / Forecast.Solar lundi 05/10* diff --git a/dashboards/evcs_master.yaml b/dashboards/evcs_master.yaml index aee2017..63bc7d8 100755 --- a/dashboards/evcs_master.yaml +++ b/dashboards/evcs_master.yaml @@ -276,7 +276,7 @@ views: {% if a is none or a.state == 'aucun' %} Aucune question en attente.{% if a is not none and a.attributes.dernier_cas %} Dernier arbitrage : {{ a.attributes.dernier_cas }} ({{ a.attributes.derniere_issue }}).{% endif %} {% else %} - {% if a.state == 'question' %}❓ **Question envoyée par WhatsApp**{% else %}✅ **Réponse reçue**{% endif %} (depuis {{ as_timestamp(a.attributes.depuis) | timestamp_custom('%H:%M') if a.attributes.depuis else '?' }}) + {% if a.state == 'question' %}❓ **Question envoyée par WhatsApp**{% elif a.state == 'alerte' %}⚠️ **Alerte envoyée par WhatsApp** (rien à répondre){% else %}✅ **Réponse reçue**{% endif %} (depuis {{ as_timestamp(a.attributes.depuis) | timestamp_custom('%H:%M') if a.attributes.depuis else '?' }}) {{ a.attributes.question }} diff --git a/tests/arbitrage/run.mjs b/tests/arbitrage/run.mjs index 37d6b2d..147e097 100644 --- a/tests/arbitrage/run.mjs +++ b/tests/arbitrage/run.mjs @@ -17,6 +17,20 @@ ok('edf-ouvert-rien', detecterSituation({ ...nuit0810, contacteurOn: false }) == ok('voiture-pleine-rien', detecterSituation({ ...nuit0810, etat: 'night_recovery_done_idle' }) === null); ok('voiture-en-charge-rien', detecterSituation({ ...nuit0810, borneW: 1400, sourceMode: 'Réseau forcé' }) === null); ok('pas-de-voiture-rien', detecterSituation({ ...nuit0810, voitureBranchee: false }) === null); +// Elargissement du 10/10 : releve du 09/10 13:59, JEEP en « Reseau HC autorise », attente. +const midi0910 = { ...nuit0810, fenetre: 'midi', finFenetre: '14:24', sourceMode: 'Réseau HC autorisé', etat: 'idle_wait_conditions', vehicule: 'JEEP' }; +const m2 = detecterSituation(midi0910); +ok('midi-0910-jeep-attend-question', m2 && m2.cas === 'voiture_bloquee_edf_ferme' && m2.choix[0].ordre.valeur === 'force_grid_now', JSON.stringify(m2)); +const n2 = detecterSituation({ ...nuit0810, sourceMode: 'Réseau forcé', etat: 'idle_wait_conditions', modeNuit: 'complete' }); +ok('nuit-hors-sans-edf-force-grid', n2 && n2.choix[0].ordre.ordre === 'strategie', JSON.stringify(n2)); +// Cas 2 : releve du 09/10 13:59, 32 A pour 140 A, 12,9 kWh a reprendre. +const rech = { fenetre: 'midi', finFenetre: '14:24', contacteurOn: true, voitureBranchee: false, borneW: 0, sourceMode: 'Réseau HC autorisé', etat: 'x', + sessionEngagee: true, manqueKwh: 12.9, soc: 57, batterieA: 32, dvccA: 140 }; +const a2 = detecterSituation(rech); +ok('recharge-bridee-alerte', a2 && a2.cas === 'recharge_bridee' && a2.choix.length === 0 && /32 A pour 140 A/.test(a2.question) && /12.9 kWh/.test(a2.question), JSON.stringify(a2)); +ok('recharge-normale-rien', detecterSituation({ ...rech, batterieA: 121 }) === null); +ok('cible-atteinte-hold-rien', detecterSituation({ ...rech, manqueKwh: 0, batterieA: 2 }) === null); +ok('hors-session-rien', detecterSituation({ ...rech, sessionEngagee: false }) === null); ok('choix-1', lireChoix('1', s.choix)?.cle === '1'); ok('choix-texte', lireChoix('je prends le 2', s.choix)?.cle === '2'); ok('choix-inconnu', lireChoix('5', s.choix) === null && lireChoix('oui', s.choix) === null); diff --git a/tools/arbitrage.mjs b/tools/arbitrage.mjs index 96de3df..11793d5 100644 --- a/tools/arbitrage.mjs +++ b/tools/arbitrage.mjs @@ -27,28 +27,52 @@ const VOITURE_ABSENTE_OU_PLEINE = /not_connected|done|full|complete|charged/i; * 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) * @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 1 — voiture branchee, non pleine, bloquee, alors qu'EDF est deja ferme - // en heures creuses. Cause de la nuit du 07-08/10 (VW bloquee 01:24-05:14). + // 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 - && /bloqu/i.test(e.sourceMode || '') && !VOITURE_ABSENTE_OU_PLEINE.test(e.etat || '')) { + && !VOITURE_ABSENTE_OU_PLEINE.test(e.etat || '')) { const v = e.vehicule || 'la voiture'; const nuit = e.fenetre === 'nuit'; - if (nuit && e.modeNuit === 'complete') return null; // deja autorisee : ce n'est pas un choix + 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: [ - nuit + 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; } diff --git a/tools/historise-evcs.mjs b/tools/historise-evcs.mjs index f0f4388..d01653a 100644 --- a/tools/historise-evcs.mjs +++ b/tools/historise-evcs.mjs @@ -2129,6 +2129,11 @@ async function arbitrer(sql, client, st) { raison: txt(st.get('input_text.victron_evcs_surplus_reason')?.state), vehicule: txt(st.get('input_select.victron_evcs_vehicle')?.state), modeNuit: txt(st.get('input_select.victron_evcs_night_charge_mode')?.state), + sessionEngagee: txt(st.get('input_boolean.victron_evcs_night_grid_session_committed')?.state) === 'on', + manqueKwh: Number(st.get('input_number.evcs_balance_grid_energy_needed_kwh')?.state), + soc: Number(st.get('sensor.victron_battery_soc')?.state), + batterieA: Number(st.get('sensor.victron_battery_current')?.state), + dvccA: Number(st.get('sensor.victron_max_charge_current_setting')?.state), }); const { rows: [ouvert] } = await client.query(`SELECT * FROM arbitrage WHERE ferme IS NULL ORDER BY ouvert DESC LIMIT 1`); const publier = (etat, a) => publierCapteur('sensor.evcs_arbitrage', etat, { friendly_name: 'Arbitrage demande au proprietaire', ...a }); @@ -2155,15 +2160,17 @@ async function arbitrer(sql, client, st) { await client.query(`INSERT INTO arbitrage (id, cas, question, choix, depuis) VALUES ($1,$2,$3,$4,$5) ON CONFLICT DO NOTHING`, [id, sit.cas, sit.question, JSON.stringify(sit.choix), new Date(arbitrageCandidat.depuis)]); await journaliser(sql, 'arbitrage', `Arbitrage demande : ${sit.cas}`, { id, question: sit.question }); - await publier('question', { id, cas: sit.cas, question: sit.question, + // Sans choix = ALERTE (10/10) : rien a decider, seulement a signaler. + await publier(sit.choix.length ? 'question' : 'alerte', { id, cas: sit.cas, question: sit.question, choix: sit.choix.map((c) => ({ cle: c.cle, libelle: c.libelle })), depuis: new Date(arbitrageCandidat.depuis).toISOString(), - consigne: 'Repondre par le numero du choix. Seul l ordre attache au choix est applique.' }); + consigne: sit.choix.length ? 'Repondre par le numero du choix. Seul l ordre attache au choix est applique.' + : 'Alerte : rien a repondre.' }); return 1; } - // Reponse du proprietaire, par le concierge. - if (!ouvert.reponse_cle) { + // Reponse du proprietaire, par le concierge (pas pour une alerte). + if (!ouvert.reponse_cle && (ouvert.choix || []).length) { let rep = null; try { rep = JSON.parse(txt(st.get('input_text.evcs_arbitrage_reponse')?.state) || 'null'); } catch { rep = null; } if (rep && rep.id === ouvert.id) {