JEEP branchee a midi : reseau en HC seulement si le solaire ne suffit pas avant son depart

Regle du proprietaire (28/09). Le bilan compare le besoin saisi (niveau x capacite
mesuree - deja delivre) au solaire disponible avant le depart de 15:30 (mar/mer/
ven/sam) sans descendre la batterie sous le plancher « tenir jusqu'a la prochaine
HC ». Le controleur relaie le verdict sous les gardes du bilan ; session de midi
engagee -> voiture sur le reseau avec la batterie. Bilan 61/61, golden 121/121.

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:
Tallagrand Thierry
2026-09-28 13:55:40 +02:00
parent 9b1c9d30cf
commit 3b9b67268c
17 changed files with 501 additions and 14 deletions

View File

@@ -982,5 +982,38 @@ la prochaine HC (01:24) ; sinon la HC de midi la complete, voiture comprise.**
consulte pas le besoin de la nuit ; le plancher du bilan n'agit au demarrage que
dans la fenetre du soir.
### 28/09 midi — JEEP branchee : le reseau en HC de midi quand le solaire ne suffit pas avant son depart
Regle du proprietaire : **« JEEP branchee engage le reseau en HC midi si l'analyse de
production le justifie »**. La JEEP repart a **15:30 mar/mer/ven/sam** (MANUEL §0) ;
les autres jours elle reste branchee et la HC de 01:24 finit la charge au meme tarif —
fermer le contacteur a midi n'y gagnerait rien et etoufferait le solaire.
- **Donnees (HA, `victron_evcs_vehicles.yaml`)** : niveau de la voiture SAISI
(`input_number.victron_evcs_vehicle_level_pct`, aucun capteur ne le donne ; retenu
seulement s'il date du branchement, tolerance 1 h) ; compteur de la borne memorise a
la saisie par une automatisation (`..._level_ref_kwh`) pour deduire ce qui a deja ete
delivre ; capacites MESUREES au compteur (JEEP 8,7 = plus grosse session, VW 7,7) ;
jours et heure de depart. Carte EVCS Master.
- **Analyse (bilan, `vehicleMidday`)** : en fenetre HC avec un depart avant la prochaine
HC, besoin = (100 - niveau) % x capacite - deja delivre, borne a 3,7 kW x temps
restant ; solaire disponible = batterie au-dessus du plancher « tenir jusqu'a la
prochaine HC » evalue AU DEPART (difference SIGNEE : une batterie sous son plancher
est d'abord remontee) + net prevu d'ici le depart (p50). Verdict si manque > 0,5 kWh
(3x le cout d'une fermeture du contacteur). Publie `input_boolean.evcs_balance_vehicle_grid_needed`,
`evcs_balance_vehicle_need_kwh`, `evcs_balance_vehicle_solar_kwh` + phrase dans la raison.
- **Controleur** : `middayGridSessionUncovered` inclut le verdict (memes gardes statut/
fraicheur que la cible, voiture branchee) ; session engagee + contacteur ferme a midi
-> voiture en `grid_hc_allowed` meme au soleil (la consigne garde son credit solaire).
- Tests : bilan 61/61 (6 scenarios JEEP, cas ensoleille verifie a la main : 4,05 + 4,96
= 9,01 kWh >= 7,66), golden 121/121 (5 scenarios), mutations qui mordent (2 bilan,
3 controleur). Deploye 13:53, cycle 13:55 propre (lundi : verdict off).
- Rejeu du 28/09 13:55 « si c'etait un jour de depart », JEEP a 12 % : SOC 48 -> solaire
8,3 kWh pour 5,9 absorbables -> pas de reseau ; SOC 35 -> manque 1,4 -> reseau.
- ⚠️ Le point 1 envisage (« la JEEP bridee a 13 A par la priorite batterie ») etait une
ERREUR de lecture : c'etait la rampe de demarrage (+1 A / 30 s), 19 A six minutes plus
tard. Rien a changer.
- ⚠️ A surveiller : le niveau doit etre saisi a chaque branchement (sinon la regle se
tait) ; la capacite JEEP 8,7 est a affiner sur une charge complete depuis un niveau
connu.
---
*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*

View File

@@ -219,6 +219,18 @@ views:
name: Heure départ VW
- entity: input_datetime.victron_evcs_departure_time_jeep
name: Heure départ JEEP
- entity: input_number.victron_evcs_vehicle_level_pct
name: 🔋 Niveau de la voiture (à saisir au branchement)
- entity: input_datetime.victron_evcs_jeep_midday_departure
name: JEEP repart après midi à
- entity: input_text.victron_evcs_jeep_midday_departure_days
name: Jours où la JEEP repart (lun mar mer jeu ven sam dim)
- entity: input_boolean.evcs_balance_vehicle_grid_needed
name: Réseau justifié pour la voiture à midi (verdict du bilan)
- entity: input_number.evcs_balance_vehicle_need_kwh
name: Besoin voiture avant départ
- entity: input_number.evcs_balance_vehicle_solar_kwh
name: Solaire disponible pour la voiture avant départ
- entity: input_boolean.victron_evcs_allow_grid_departure_urgent
name: ⚡ Réseau avant départ (P9)
- entity: input_select.victron_evcs_strategy
@@ -615,3 +627,25 @@ views:
attribute: cause
name: Cause du dernier échec
icon: mdi:alert-outline
# ----------------------------------------------------------------
# Interventions borne & alertes (19/09/2026)
# Le swap de vehicule trop rapide (14 s) fige la borne et bloque le
# nouveau vehicule. P11-bis (last_changed) le corrige tout seul ; ce
# bouton reste le filet MANUEL si la borne se coince malgre tout.
# L'alerte "branchee mais ne charge pas" est OPT-IN, seuils a calibrer.
# ----------------------------------------------------------------
- type: entities
title: Interventions borne & alertes
entities:
- entity: input_boolean.evcs_reset_borne
name: Reset borne (forcer un retest)
icon: mdi:restart-alert
- type: divider
- entity: input_boolean.evcs_alerte_charge_enabled
name: Envoyer les alertes WhatsApp
icon: mdi:bell-alert
- entity: input_text.evcs_alerte_charge
name: Alerte en cours (vide = rien)
icon: mdi:ev-station
secondary_info: last-changed

View File

@@ -194,6 +194,14 @@ appliquer les 7,70 kWh de la VW.
Exemples donnes : lundi midi la JEEP · mardi midi la JEEP puis la VW la nuit ·
mercredi midi la JEEP.
**Depart de la JEEP apres le dejeuner (donne le 28/09/2026)** : elle **repart a
15:30 le mardi, le mercredi, le vendredi et le samedi** ; elle **ne repart pas**
le lundi, le jeudi ni le dimanche (elle reste branchee). Ce calendrier vit dans
HA (`input_text.victron_evcs_jeep_midday_departure_days`,
`input_datetime.victron_evcs_jeep_midday_departure`) : les jours de depart, ce
que la JEEP n'a pas pris avant 15:30 ne pourra pas l'etre en HC la nuit — c'est
ce qui peut justifier le reseau en HC de midi (HANDOFF §81).
**LE WEEK-END, LA ROUTINE S'INVERSE.** Sa femme travaille le samedi matin et le
dimanche matin, retour a midi : la **JEEP** charge donc le midi ET la nuit du
samedi au dimanche. La **VW** ne bouge pas du week-end — sauf sortie plongee —

View File

@@ -13,6 +13,12 @@ input_boolean:
evcs_balance_grid_needed:
name: "Bilan — réseau nécessaire"
icon: mdi:transmission-tower
# Voiture branchee en HC avec un depart avant la prochaine HC (28/09) : le
# solaire ne suffit pas a lui donner son besoin avant le depart -> le
# controleur engage la session reseau de la fenetre HC.
evcs_balance_vehicle_grid_needed:
name: "Bilan — réseau justifié pour la voiture (départ avant la prochaine HC)"
icon: mdi:car-electric
evcs_balance_pv_censored:
name: "Bilan — production actuellement bridée"
icon: mdi:solar-power-variant-outline
@@ -75,6 +81,22 @@ input_number:
icon: mdi:battery-clock
# Regle « tenir jusqu'a la prochaine HC » (28/09) : SOC a avoir en fin de
# fenetre HC en cours. 0 = hors fenetre HC ou regle sans effet.
evcs_balance_vehicle_need_kwh:
name: "Bilan — besoin de la voiture avant son départ"
min: 0
max: 100
step: 0.01
unit_of_measurement: kWh
mode: box
icon: mdi:car-arrow-left
evcs_balance_vehicle_solar_kwh:
name: "Bilan — solaire disponible pour la voiture avant son départ"
min: 0
max: 100
step: 0.01
unit_of_measurement: kWh
mode: box
icon: mdi:solar-power-variant
evcs_balance_hc_floor_pct:
name: "Bilan — plancher HC (prochaine HC)"
min: 0

View File

@@ -37,8 +37,84 @@ input_datetime:
name: EVCS - Heure départ JEEP
has_date: false
has_time: true
victron_evcs_jeep_midday_departure:
name: EVCS - Heure de départ JEEP après midi
has_date: false
has_time: true
input_boolean:
victron_evcs_allow_grid_departure_urgent:
name: EVCS - Réseau avant départ (P9)
icon: mdi:car-clock
# =====================================================
# JEEP BRANCHEE A MIDI (28/09/2026, strategie du proprietaire)
# « JEEP branchee engage le reseau en HC midi si l'analyse de production le
# justifie ». Le bilan energetique compare le besoin de la voiture a ce que le
# solaire peut lui donner AVANT son depart sans descendre la batterie sous le
# plancher « tenir jusqu'a la prochaine HC ». Aucun capteur ne donne le niveau
# de la voiture : le proprietaire le saisit (tableau de bord ou agent vocal).
# Le calendrier reste ici : le bilan ne recoit qu'un instant de depart.
# =====================================================
input_number:
victron_evcs_vehicle_level_pct:
name: EVCS - Niveau de charge du véhicule (saisi)
min: 0
max: 100
step: 1
unit_of_measurement: "%"
mode: box
icon: mdi:car-battery
# Compteur de la borne au moment de la saisie du niveau (ecrit par
# l'automatisation ci-dessous) : ce que la voiture a pris DEPUIS se deduit.
victron_evcs_vehicle_level_ref_kwh:
name: EVCS - Compteur borne à la saisie du niveau
min: 0
max: 1000000
step: 0.1
unit_of_measurement: kWh
mode: box
icon: mdi:counter
# Capacites MESUREES au compteur de la borne (charge complete depuis un
# niveau bas), pas les valeurs constructeur. VW 7,70 (15/09, MANUEL §0) ;
# JEEP 8,7 = plus grosse session relevee (22/09), a affiner.
victron_evcs_capacity_jeep_kwh:
name: EVCS - Capacité mesurée JEEP
min: 1
max: 100
step: 0.1
unit_of_measurement: kWh
mode: box
icon: mdi:car-battery
victron_evcs_capacity_vw_kwh:
name: EVCS - Capacité mesurée VW
min: 1
max: 100
step: 0.1
unit_of_measurement: kWh
mode: box
icon: mdi:car-battery
input_text:
# Jours ou la JEEP repart apres le dejeuner (lun mar mer jeu ven sam dim).
victron_evcs_jeep_midday_departure_days:
name: EVCS - Jours de départ JEEP après midi
max: 60
icon: mdi:calendar-week
automation:
- id: evcs_niveau_vehicule_reference
alias: "EVCS — mémoriser le compteur de la borne à la saisie du niveau"
mode: queued
trigger:
- platform: state
entity_id: input_number.victron_evcs_vehicle_level_pct
condition:
- condition: template
value_template: "{{ states('sensor.evcs_hq2239dnt7p_total_energy') | is_number }}"
action:
- service: input_number.set_value
target:
entity_id: input_number.victron_evcs_vehicle_level_ref_kwh
data:
value: "{{ states('sensor.evcs_hq2239dnt7p_total_energy') | float }}"

View File

@@ -301,14 +301,14 @@ const hcNightReachActive = Number.isFinite(hcWindowEndMs) && Number.isFinite(nex
// Energie a avoir en reserve a la fin de la fenetre : parcours A REBOURS des
// tranches comprises entre les deux instants (au prorata de leur recouvrement).
// Un surplus ne rembourse qu'un deficit POSTERIEUR — d'ou le sens du parcours.
function hcReachFloorPct(slots, untilMs = nextHcStartMs) {
if (!Number.isFinite(hcWindowEndMs) || !Number.isFinite(untilMs) || untilMs <= hcWindowEndMs) return 0;
function hcReachFloorPct(slots, untilMs = nextHcStartMs, fromMs = hcWindowEndMs) {
if (!Number.isFinite(fromMs) || !Number.isFinite(untilMs) || untilMs <= fromMs) return 0;
let needKwh = 0;
for (let i = slots.length - 1; i >= 0; i--) {
const s = slots[i];
const debut = num(s.startMs, NaN);
if (!Number.isFinite(debut)) continue;
const a = Math.max(debut, hcWindowEndMs);
const a = Math.max(debut, fromMs);
const b = Math.min(debut + 3600000, untilMs);
if (b <= a) continue;
needKwh = Math.max(0, needKwh - s.netKwh * (b - a) / 3600000);
@@ -348,6 +348,77 @@ const hcFloorPrudentPct = Math.max(hcReachFloorPct(slotsP80, nextHcStartMs), hcF
const typical = coverage(slotsP50, hcFloorTypicalPct);
const prudent = coverage(slotsP80, hcFloorPrudentPct);
// ---------------------------------------------------------------------
// VOITURE BRANCHEE EN HC AVEC UN DEPART AVANT LA PROCHAINE HC (28/09/2026)
//
// Regle du proprietaire : « JEEP branchee engage le reseau en HC midi si
// l'analyse de production le justifie ». La JEEP repart a 15:30 certains jours
// (calendrier dans HA) : ce qu'elle n'a pas pris avant, elle ne le prendra pas
// en HC la nuit. Ailleurs, elle reste branchee et la HC de 01:24 la finit au
// meme tarif — fermer le contacteur a midi n'y gagnerait rien et etoufferait le
// solaire (installation sans injection).
//
// Ce que le solaire peut donner a la voiture avant son depart SANS toucher a
// la reserve de la nuit : l'energie de la batterie au-dessus du plancher
// « tenir jusqu'a la prochaine HC » evalue AU DEPART, plus le bilan net prevu
// (production - maison) d'ici le depart. Profil p50, comme required_soc.
// Le reseau est justifie quand le besoin de la voiture, borne par ce que son
// chargeur peut absorber avant le depart, depasse ce solaire.
//
// Besoin = (100 - niveau saisi) % x capacite MESUREE au compteur de la borne,
// moins ce que la borne a deja delivre depuis la saisie. Aucun capteur ne donne
// le niveau de la voiture : sans saisie valable, la regle se tait.
//
// Deux constantes, justifiees :
// - 3,7 kW : plateau MESURE des deux vehicules du foyer (3 655 et 3 680 W a
// consigne pleine, MANUEL §0) ; au-dela la voiture n'absorbe rien de plus.
// - 0,5 kWh : seuil de manque en dessous duquel une session ne se justifie
// pas — une fermeture du contacteur coute 50 a 150 Wh de transitoire (§57),
// on exige un gain d'au moins trois fois ce cout.
const VEHICLE_CHARGER_KW = 3.7;
const VEHICLE_GRID_MIN_SHORTFALL_KWH = 0.5;
const nowMsBalance = num(input.nowMs, Date.now());
const vehicleConnected = input.vehicle_connected === true || input.vehicle_connected === 'true';
const vehicleDepartureMs = num(input.vehicle_departure_ms, NaN);
const vehicleLevelPct = num(input.vehicle_level_pct, NaN);
const vehicleLevelValid = input.vehicle_level_valid === true || input.vehicle_level_valid === 'true';
const vehicleCapacityKwh = num(input.vehicle_capacity_kwh, NaN);
const vehicleDeliveredKwh = Math.max(0, num(input.vehicle_delivered_since_level_kwh, 0));
const vehicleNeedKwh =
vehicleLevelValid && Number.isFinite(vehicleLevelPct) && vehicleLevelPct >= 0 && vehicleLevelPct <= 100
&& Number.isFinite(vehicleCapacityKwh) && vehicleCapacityKwh > 0
? Math.max(0, (100 - vehicleLevelPct) / 100 * vehicleCapacityKwh - vehicleDeliveredKwh)
: NaN;
const vehicleRuleActive =
hcReachActive && vehicleConnected && Number.isFinite(vehicleNeedKwh)
&& Number.isFinite(vehicleDepartureMs) && vehicleDepartureMs > nowMsBalance
&& vehicleDepartureMs < nextHcStartMs;
let vehicleSolarKwh = null;
let vehicleReachableKwh = null;
let vehicleShortfallKwh = null;
let vehicleFloorAtDeparturePct = null;
if (vehicleRuleActive) {
vehicleFloorAtDeparturePct = hcReachFloorPct(slotsP50, nextHcStartMs, vehicleDepartureMs);
let netKwh = 0;
for (const sl of slotsP50) {
const debut = num(sl.startMs, NaN);
if (!Number.isFinite(debut)) continue;
const a = Math.max(debut, nowMsBalance);
const b = Math.min(debut + 3600000, vehicleDepartureMs);
if (b > a) netKwh += sl.netKwh * (b - a) / 3600000;
}
// Difference SIGNEE : une batterie SOUS son plancher doit d'abord etre
// remontee par ce meme surplus avant que la voiture n'en voie un kWh.
const plancherDepart = Math.max(socFloorPct, vehicleFloorAtDeparturePct);
const batterieAuDessusKwh = socPct >= plancherDepart
? energyBetweenSoc(plancherDepart, socPct)
: -energyBetweenSoc(socPct, plancherDepart);
vehicleSolarKwh = Math.max(0, batterieAuDessusKwh + netKwh);
vehicleReachableKwh = Math.min(vehicleNeedKwh, VEHICLE_CHARGER_KW * (vehicleDepartureMs - nowMsBalance) / 3600000);
vehicleShortfallKwh = Math.max(0, vehicleReachableKwh - vehicleSolarKwh);
}
const vehicleGridNeeded = vehicleRuleActive && vehicleShortfallKwh > VEHICLE_GRID_MIN_SHORTFALL_KWH;
// ---------------------------------------------------------------------
// PROCHAINE FENÊTRE DE DÉFICIT (ajout du 24/08/2026)
//
@@ -537,6 +608,18 @@ return [{
surplusAvailableKwh: round2(surplusAfterCrossingKwh),
},
// Voiture branchee en HC, depart avant la prochaine HC (28/09).
vehicleMidday: {
active: vehicleRuleActive,
needKwh: Number.isFinite(vehicleNeedKwh) ? round2(vehicleNeedKwh) : null,
departureMs: Number.isFinite(vehicleDepartureMs) ? vehicleDepartureMs : null,
solarKwh: vehicleSolarKwh === null ? null : round2(vehicleSolarKwh),
reachableKwh: vehicleReachableKwh === null ? null : round2(vehicleReachableKwh),
shortfallKwh: vehicleShortfallKwh === null ? null : round2(vehicleShortfallKwh),
floorAtDeparturePct: vehicleFloorAtDeparturePct === null ? null : round1(vehicleFloorAtDeparturePct),
gridNeeded: vehicleGridNeeded,
},
// Tenir jusqu'a la prochaine fenetre HC (28/09) : SOC a avoir en fin de
// fenetre HC en cours. 0 hors fenetre ou si la production suffit.
hcReach: {

View File

@@ -58,6 +58,8 @@ setText('input_text.evcs_balance_last_run', stampParis());
// --- Le bilan lui-même ---------------------------------------------------
if (!balance.usable) {
setText('input_text.evcs_balance_status', 'indisponible');
// Jamais de verdict voiture perime quand le bilan ne sait plus rien.
setBool('input_boolean.evcs_balance_vehicle_grid_needed', false);
setText('input_text.evcs_balance_reason',
`Entrées manquantes : ${(balance.missing || []).join(', ')}. Les contrôleurs gardent leurs règles.`);
return calls;
@@ -85,6 +87,11 @@ setNumber('input_number.evcs_balance_required_soc_prudent', c.prudent.requiredSo
// Regle HC (28/09) publiee pour etre verifiable : 0 = hors fenetre HC.
setNumber('input_number.evcs_balance_hc_floor_pct', balance.hcReach ? balance.hcReach.floorPct : 0);
setNumber('input_number.evcs_balance_hc_floor_prudent_pct', balance.hcReach ? balance.hcReach.floorPrudentPct : 0);
// Voiture branchee en HC, depart avant la prochaine HC (28/09). OFF hors regle.
const vm = balance.vehicleMidday || {};
setBool('input_boolean.evcs_balance_vehicle_grid_needed', !!vm.gridNeeded);
setNumber('input_number.evcs_balance_vehicle_need_kwh', vm.active ? vm.reachableKwh : 0);
setNumber('input_number.evcs_balance_vehicle_solar_kwh', vm.active ? vm.solarKwh : 0);
setNumber('input_number.evcs_balance_energy_to_cover_kwh', c.energyToCoverKwh);
setNumber('input_number.evcs_balance_grid_energy_needed_kwh', c.gridEnergyNeededKwh);
setNumber('input_number.evcs_balance_vehicle_surplus_kwh', balance.vehicle.surplusAvailableKwh);
@@ -143,7 +150,11 @@ const hc = balance.hcReach || {};
const hcText = hc.active && hc.raisesRequirement
? ` Tenir jusqu'à la prochaine HC (${new Date(hc.nextHcStartMs).toLocaleTimeString('fr-FR', { timeZone: 'Europe/Paris', hour: '2-digit', minute: '2-digit' })}) → ${hc.floorPct}% en fin de fenêtre.`
: '';
setText('input_text.evcs_balance_reason', reason + hcText + degradedNote + ndText);
const hhmm = ms => new Date(ms).toLocaleTimeString('fr-FR', { timeZone: 'Europe/Paris', hour: '2-digit', minute: '2-digit' });
const vehText = vm.active
? ` Voiture (départ ${hhmm(vm.departureMs)}) : ${vm.reachableKwh} kWh à donner, solaire ${vm.solarKwh} kWh → ${vm.gridNeeded ? 'réseau justifié' : 'solaire suffisant'}.`
: '';
setText('input_text.evcs_balance_reason', reason + hcText + vehText + degradedNote + ndText);
// Trace longue durée, pour comparer jour après jour le seuil calculé au seuil
// fixe encore en vigueur — c'est cette comparaison qui décidera du câblage.

View File

@@ -20,8 +20,34 @@
{%- if w[0] >= repere and ns.suivant == '' -%}{%- set ns.suivant = w[0] -%}{%- endif -%}
{%- if w[0] >= repere and w[2] == 'nuit' and ns.nuit == '' -%}{%- set ns.nuit = w[0] -%}{%- endif -%}
{%- endfor -%}
{#- Voiture branchee a midi (28/09/2026). Le calendrier (jours et heure de
depart de la JEEP apres le dejeuner) reste dans HA : le bilan recoit un
instant de depart, vide si la voiture ne repart pas aujourd'hui. Le niveau
saisi n'est retenu que s'il date de ce branchement (tolerance 1 h avant). -#}
{%- set vh = namespace(dep='', valide=false, cap=none) -%}
{%- set jours = ['lun', 'mar', 'mer', 'jeu', 'ven', 'sam', 'dim'] -%}
{%- if is_state('input_select.victron_evcs_vehicle', 'JEEP') -%}
{%- set vh.cap = states('input_number.victron_evcs_capacity_jeep_kwh') -%}
{%- if jours[now().weekday()] in states('input_text.victron_evcs_jeep_midday_departure_days') | lower -%}
{%- set d = today_at(states('input_datetime.victron_evcs_jeep_midday_departure')) -%}
{%- if d > now() -%}{%- set vh.dep = d -%}{%- endif -%}
{%- endif -%}
{%- elif is_state('input_select.victron_evcs_vehicle', 'VW') -%}
{%- set vh.cap = states('input_number.victron_evcs_capacity_vw_kwh') -%}
{%- endif -%}
{%- set branche = states.binary_sensor.victron_evcs_vehicle_connected -%}
{%- set niveau = states.input_number.victron_evcs_vehicle_level_pct -%}
{%- if branche is not none and niveau is not none and branche.state == 'on' -%}
{%- set vh.valide = as_timestamp(niveau.last_changed) >= as_timestamp(branche.last_changed) - 3600 -%}
{%- endif -%}
{{ {
"ts": now().isoformat(),
"vehicle_connected": is_state("binary_sensor.victron_evcs_vehicle_connected", "on"),
"vehicle_departure_ms": (as_timestamp(vh.dep) * 1000) if vh.dep != '' else none,
"vehicle_level_pct": states("input_number.victron_evcs_vehicle_level_pct"),
"vehicle_level_valid": vh.valide,
"vehicle_capacity_kwh": vh.cap,
"vehicle_delivered_since_level_kwh": ((states("sensor.evcs_hq2239dnt7p_total_energy") | float(0)) - (states("input_number.victron_evcs_vehicle_level_ref_kwh") | float(0))) if states("sensor.evcs_hq2239dnt7p_total_energy") | is_number else none,
"hc_window_end_ms": (as_timestamp(ns.fin) * 1000) if ns.fin != '' else none,
"next_hc_start_ms": (as_timestamp(ns.suivant) * 1000) if ns.suivant != '' else none,
"next_night_hc_start_ms": (as_timestamp(ns.nuit) * 1000) if ns.nuit != '' else none,

View File

@@ -60,6 +60,13 @@ return [{
// Plancher de charge HC manuelle (18/09) : oublie ici jusqu'au 28/09, le
// bouton n'avait donc jamais atteint le bilan.
manual_hc_floor_percent: snapshot.manual_hc_floor_percent,
// Voiture branchee a midi (28/09) : depart du jour et besoin saisi.
vehicle_connected: snapshot.vehicle_connected,
vehicle_departure_ms: snapshot.vehicle_departure_ms,
vehicle_level_pct: snapshot.vehicle_level_pct,
vehicle_level_valid: snapshot.vehicle_level_valid,
vehicle_capacity_kwh: snapshot.vehicle_capacity_kwh,
vehicle_delivered_since_level_kwh: snapshot.vehicle_delivered_since_level_kwh,
hc_window_end_ms: snapshot.hc_window_end_ms,
next_hc_start_ms: snapshot.next_hc_start_ms,
next_night_hc_start_ms: snapshot.next_night_hc_start_ms,

View File

@@ -153,6 +153,7 @@
"dvcc_max_charge_current_a": states("sensor.victron_max_charge_current_setting"),
"balance_status": states("input_text.evcs_balance_status"),
"balance_last_run": states("input_text.evcs_balance_last_run"),
"balance_vehicle_grid_needed": states("input_boolean.evcs_balance_vehicle_grid_needed"),
"allow_grid_hc_midday": states("input_boolean.victron_evcs_allow_grid_hc_midday"),
"probe_enabled": states("input_boolean.victron_evcs_probe_enabled"),

View File

@@ -487,6 +487,9 @@ const params = {
// BILAN ENERGETIQUE (03/09/2026). Meme format d'horodatage que l'advisor
// ("DD/MM HH:mm"), d'ou la reutilisation du parseur.
balanceRequiredSocPct: num(input.balance_required_soc, NaN),
// Voiture branchee en HC avec un depart avant la prochaine HC (28/09) : le
// bilan juge que le solaire ne lui donnera pas son besoin avant le depart.
balanceVehicleGridNeeded: low(input.balance_vehicle_grid_needed || 'off') === 'on',
// P12 — charge nuit sur batterie. Le plancher est le SOC requis PRUDENT (p80)
// et non le p50 : la marge est mesuree, pas inventee. On descend la voiture
// jusqu'a ce que la batterie n'ait plus que ce qu'il lui faut pour tenir

View File

@@ -739,10 +739,24 @@ const nightGridSessionUncovered =
// `required_soc` = 22,0 pour `energy_to_cover` = 0,0. Il ne remonte qu'un jour
// sombre — ou quand le proprietaire pose un plancher manuel, ce qui est
// exactement le cas d'usage.
//
// ET LA VOITURE (28/09/2026, strategie du proprietaire) : « JEEP branchee
// engage le reseau en HC midi si l'analyse de production le justifie ». Le
// bilan fait l'analyse (besoin saisi contre solaire disponible avant le depart,
// batterie gardee pour tenir jusqu'a 01:24) et publie un verdict ; on le relaie,
// sous les memes gardes de fraicheur et de statut que la cible.
const middayVehicleGridNeeded =
balanceMorningTargetApplied &&
params.balanceVehicleGridNeeded &&
parsed.evcsVehicleConnected;
const middayGridSessionUncovered =
balanceMorningTargetApplied &&
Number.isFinite(params.balanceRequiredSocPct) &&
parsed.soc < params.balanceRequiredSocPct;
(
(Number.isFinite(params.balanceRequiredSocPct) &&
parsed.soc < params.balanceRequiredSocPct) ||
middayVehicleGridNeeded
);
const gridSessionInHcWindow =
profile === 'night_hc' || profile === 'midday_hc_or_solar';
@@ -931,7 +945,15 @@ else if (profile === 'night_hc') {
}
else if (profile === 'midday_hc_or_solar' || profile === 'morning_solar') {
if (strongSolarSignal) {
// Session de midi engagee et contacteur ferme : la voiture charge sur le
// reseau avec la batterie (« batterie et voiture ensemble », 28/09), meme au
// soleil. La consigne garde son credit solaire (HA_SERVICE_CALLS_OUTPUT) :
// elle ne preleve au reseau que ce que les chargeurs ne donnent pas.
if (profile === 'midday_hc_or_solar' && nightGridSessionCommitted && parsed.gridAvailable) {
sourceMode = 'grid_hc_allowed';
}
else if (strongSolarSignal) {
sourceMode = 'solar_only';
}
@@ -1168,6 +1190,7 @@ return [{
nightBatterySurplusAvailable,
nightBatterySurplusExhausted,
nightGridSessionCommitted,
middayVehicleGridNeeded,
nightGridSessionUncovered,
balanceTargetAgeMin:
balanceTargetAgeMin === null ? null : Math.round(balanceTargetAgeMin * 10) / 10,

View File

@@ -106,6 +106,13 @@ const SUMMARIZE = {
hcReachActive: o.hcReach ? o.hcReach.active : false,
hcFloorPct: o.hcReach ? o.hcReach.floorPct : null,
hcNightFloorPrudentPct: o.hcReach ? o.hcReach.nightFloorPrudentPct : null,
// Voiture branchee en HC, depart avant la prochaine HC (28/09).
vehicleActive: o.vehicleMidday ? o.vehicleMidday.active : false,
vehicleNeedKwh: o.vehicleMidday ? o.vehicleMidday.needKwh : null,
vehicleReachableKwh: o.vehicleMidday ? o.vehicleMidday.reachableKwh : null,
vehicleSolarKwh: o.vehicleMidday ? o.vehicleMidday.solarKwh : null,
vehicleFloorAtDeparturePct: o.vehicleMidday ? o.vehicleMidday.floorAtDeparturePct : null,
vehicleGridNeeded: o.vehicleMidday ? o.vehicleMidday.gridNeeded : false,
}) : ({ usable: false, missing: o.missing.join(',') }),
};

View File

@@ -64,6 +64,13 @@ function snap2709(nowIso, soc, fenetre, facteur27 = 1) {
const NUIT_2709 = ['2026-09-27T07:24:00+02:00', '2026-09-27T12:24:00+02:00', '2026-09-28T01:24:00+02:00'];
const MIDI_2709 = ['2026-09-27T14:24:00+02:00', '2026-09-28T01:24:00+02:00', '2026-09-28T01:24:00+02:00'];
const SOMBRE = 18 / 25.1;
// JEEP branchee a midi, depart 15:30 (28/09) : niveau saisi, capacite mesuree
// 8,7 kWh, rien de delivre depuis la saisie sauf mention.
function jeep(snapshot, niveau, extra = {}) {
return Object.assign(snapshot, { vehicle_connected: true,
vehicle_departure_ms: Date.parse('2026-09-27T15:30:00+02:00'), vehicle_level_pct: niveau,
vehicle_level_valid: true, vehicle_capacity_kwh: 8.7, vehicle_delivered_since_level_kwh: 0 }, extra);
}
export const balanceScenarios = [
{
@@ -432,5 +439,54 @@ export const balanceScenarios = [
label: 'Hors fenetre HC : aucune exigence ajoutee (22 %, pas de reseau)',
snapshot: snap2709('2026-09-27T12:30:00+02:00', 30, null, SOMBRE),
expect: { usable: true, requiredSocPct: 22, gridNeeded: false, hcReachActive: false, hcFloorPct: 0 },
}, // ======== JEEP BRANCHEE A MIDI, DEPART 15:30 (28/09/2026) ========
{
node: 'BALANCE',
id: 'bilan-jeep-midi-soleil-solaire-suffit',
label: 'JEEP a 12 %, midi ensoleille (prevision 25,1 kWh), SOC 47 : le solaire couvre, pas de reseau',
// Verifie a la main : 88 % x 8,7 = 7,66 kWh. Plancher au depart 33,5 % ->
// 13,5 points x 0,30 = 4,05 kWh de batterie + 4,96 kWh de surplus net
// 12:30-15:30 = 9,01 kWh >= 7,66.
snapshot: jeep(snap2709('2026-09-27T12:30:00+02:00', 47, MIDI_2709), 12),
expect: { usable: true, vehicleActive: true, vehicleNeedKwh: 7.66, vehicleSolarKwh: 9,
vehicleFloorAtDeparturePct: 33.5, vehicleGridNeeded: false },
},
{
node: 'BALANCE',
id: 'bilan-jeep-midi-sombre-reseau-justifie',
label: 'JEEP a 12 %, midi sombre (18 kWh), SOC 30 : le solaire ne couvre pas avant 15:30 -> reseau justifie',
// La batterie est SOUS son plancher de depart (41,4 %) : il lui manque
// 3,42 kWh, plus que les 2,26 kWh de surplus -> rien pour la voiture.
snapshot: jeep(snap2709('2026-09-27T12:30:00+02:00', 30, MIDI_2709, SOMBRE), 12),
expect: { usable: true, vehicleActive: true, vehicleSolarKwh: 0, vehicleReachableKwh: 7.66,
vehicleGridNeeded: true },
},
{
node: 'BALANCE',
id: 'bilan-jeep-midi-sans-depart-regle-muette',
label: 'Jour sans depart (lundi, jeudi, dimanche) : la regle se tait, la HC de 01:24 finira la charge',
snapshot: jeep(snap2709('2026-09-27T12:30:00+02:00', 30, MIDI_2709, SOMBRE), 12, { vehicle_departure_ms: null }),
expect: { usable: true, vehicleActive: false, vehicleGridNeeded: false },
},
{
node: 'BALANCE',
id: 'bilan-jeep-midi-niveau-perime-regle-muette',
label: 'Niveau saisi avant ce branchement : on ne devine pas le besoin, la regle se tait',
snapshot: jeep(snap2709('2026-09-27T12:30:00+02:00', 30, MIDI_2709, SOMBRE), 12, { vehicle_level_valid: false }),
expect: { usable: true, vehicleActive: false, vehicleGridNeeded: false },
},
{
node: 'BALANCE',
id: 'bilan-jeep-midi-deja-chargee-par-le-solaire',
label: 'Saisie 12 % puis 7,5 kWh deja delivres : besoin residuel 0,16 kWh, sous le seuil -> pas de reseau',
snapshot: jeep(snap2709('2026-09-27T12:30:00+02:00', 30, MIDI_2709, SOMBRE), 12, { vehicle_delivered_since_level_kwh: 7.5 }),
expect: { usable: true, vehicleActive: true, vehicleNeedKwh: 0.16, vehicleGridNeeded: false },
},
{
node: 'BALANCE',
id: 'bilan-jeep-hors-fenetre-hc-regle-muette',
label: 'Hors fenetre HC (15:00) : la regle ne s engage jamais hors HC',
snapshot: jeep(snap2709('2026-09-27T15:00:00+02:00', 30, null, SOMBRE), 12),
expect: { usable: true, vehicleActive: false, vehicleGridNeeded: false },
},
];

View File

@@ -2562,6 +2562,103 @@ export const scenarios = [
takeoverNeeded: false,
},
},
// ======== JEEP BRANCHEE A MIDI, DEPART 15:30 (28/09/2026) ========
// « JEEP branchee engage le reseau en HC midi si l'analyse de production le
// justifie » : le BILAN fait l'analyse et publie un verdict, le controleur le
// relaie sous les memes gardes (statut, fraicheur) que la cible. Ici la
// batterie n'a rien a reclamer (SOC 45 > 30) : seul le verdict voiture engage.
{
id: 'hc-midi-jeep-verdict-bilan-engage-la-session',
label: 'MIDI 28/09 — JEEP branchee, le bilan juge le solaire insuffisant avant 15:30 : la session HC de midi s engage',
time: MIDDAY,
overrides: {
evcs_vehicle_connected: 'on',
evcs_device_connected: 'on',
soc_percent: '45', battery_current_a: '10',
pv_power_w: '3000', pv_power_10m_avg_w: '3000', pv_trend_w: '0',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
evcs_ac_power_w: '0',
balance_required_soc: '30', balance_energy_to_cover_kwh: '0',
balance_grid_energy_needed_kwh: '0',
balance_status: 'ok', balance_last_run: '08/08 12:50',
balance_vehicle_grid_needed: 'on',
},
expect: { profile: 'midday_hc_or_solar', sessionCommittedService: 'turn_on', takeoverNeeded: true },
},
{
id: 'hc-midi-jeep-sans-verdict-aucun-engagement',
label: 'CONTREFACTUEL — meme midi, le bilan juge le solaire suffisant : aucune session',
time: MIDDAY,
overrides: {
evcs_vehicle_connected: 'on',
evcs_device_connected: 'on',
soc_percent: '45', battery_current_a: '10',
pv_power_w: '3000', pv_power_10m_avg_w: '3000', pv_trend_w: '0',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
evcs_ac_power_w: '0',
balance_required_soc: '30', balance_energy_to_cover_kwh: '0',
balance_grid_energy_needed_kwh: '0',
balance_status: 'ok', balance_last_run: '08/08 12:50',
balance_vehicle_grid_needed: 'off',
},
expect: { profile: 'midday_hc_or_solar', takeoverNeeded: false },
},
{
id: 'hc-midi-jeep-verdict-voiture-debranchee',
label: 'CONTREFACTUEL — verdict encore a ON mais voiture debranchee : aucune session',
time: MIDDAY,
overrides: {
evcs_vehicle_connected: 'off',
evcs_device_connected: 'on',
soc_percent: '45', battery_current_a: '10',
pv_power_w: '3000', pv_power_10m_avg_w: '3000', pv_trend_w: '0',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
evcs_ac_power_w: '0',
balance_required_soc: '30', balance_energy_to_cover_kwh: '0',
balance_grid_energy_needed_kwh: '0',
balance_status: 'ok', balance_last_run: '08/08 12:50',
balance_vehicle_grid_needed: 'on',
},
expect: { profile: 'midday_hc_or_solar', takeoverNeeded: false },
},
{
id: 'hc-midi-jeep-verdict-bilan-perime',
label: 'CONTREFACTUEL — verdict a ON mais bilan vieux de 2 h : on ne relaie pas un verdict perime',
time: MIDDAY,
overrides: {
evcs_vehicle_connected: 'on',
evcs_device_connected: 'on',
soc_percent: '45', battery_current_a: '10',
pv_power_w: '3000', pv_power_10m_avg_w: '3000', pv_trend_w: '0',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
evcs_ac_power_w: '0',
balance_required_soc: '30', balance_energy_to_cover_kwh: '0',
balance_grid_energy_needed_kwh: '0',
balance_status: 'ok', balance_last_run: '08/08 11:00',
balance_vehicle_grid_needed: 'on',
},
expect: { profile: 'midday_hc_or_solar', takeoverNeeded: false },
},
{
id: 'hc-midi-session-engagee-voiture-sur-reseau',
label: 'MIDI — session engagee, contacteur ferme, plein soleil : la voiture passe sur le reseau (batterie et voiture ensemble)',
time: MIDDAY,
overrides: {
evcs_vehicle_connected: 'on',
grid_state: 'on', victron_acin1_available: '1',
night_grid_session_committed_prev: 'on',
evcs_device_connected: 'on',
soc_percent: '45', battery_current_a: '10',
pv_power_w: '6000', pv_power_10m_avg_w: '6000', pv_trend_w: '0',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
evcs_ac_power_w: '0',
balance_required_soc: '30', balance_energy_to_cover_kwh: '0',
balance_grid_energy_needed_kwh: '0',
balance_status: 'ok', balance_last_run: '08/08 12:50',
balance_vehicle_grid_needed: 'on',
},
expect: { profile: 'midday_hc_or_solar', sourceMode: 'grid_hc_allowed', takeoverNeeded: true },
},
{
id: 'consigne-bilan-plancher-nuage-exclut-la-batterie',
label: 'CONSIGNE 18/09 — midi, soleil bride, manque batterie, PAS de voiture : le terme batterie ne doit PAS remonter le plancher nuage',

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long