Alerte BOOST : resistance jamais atteinte en HC (AUTO s'arrete sous 65 °C) ; Jeep non identifiee si elle roule apres le branchement ; relecture Uconnect au branchement
This commit is contained in:
@@ -29,6 +29,7 @@ const normalized = root.normalized ?? {};
|
||||
const observation = root.observation ?? {};
|
||||
const energyModel = root.energyModel ?? {};
|
||||
const baseDecision = root.baseDecision ?? {};
|
||||
const input = root.input ?? {};
|
||||
|
||||
let {
|
||||
recommendedMode,
|
||||
@@ -333,29 +334,37 @@ if (advisorEnabled && ecsNuit) {
|
||||
// ALERTE « PASSE EN BOOST » (07/10/2026, proprietaire)
|
||||
// =====================================================
|
||||
// En heures creuses sur EDF, le ballon doit consommer AU MAXIMUM toute la
|
||||
// fenetre : en AUTO il reste sur la pompe a chaleur (lent, 55 °C) et la douche
|
||||
// ne sera pas a 65 °C (20 h apres la HC de midi, 06 h apres celle de nuit).
|
||||
// Si, 15 min apres le debut de la fenetre, session engagee, contacteur ferme et
|
||||
// ballon sous tension, il ne tire que la puissance de la pompe a chaleur
|
||||
// (regime hp_low), on le SIGNALE : input_text.evcs_alerte_chauffe_eau (JSON,
|
||||
// `since` = debut de la fenetre -> un seul message par fenetre, envoye par le
|
||||
// concierge OpenClaw). 0 W = thermostat satisfait : pas d'alerte.
|
||||
// fenetre (douches 06:00 et 20:00 a 65 °C). Constat du 07/10 a midi : en AUTO
|
||||
// il etait ARRETE (0-36 W) alors que l'eau n'etait pas a 65 °C — passe en BOOST
|
||||
// a 12:35 il est reparti a 1 235 W. AUTO s'arrete plus bas : 0 W ne prouve
|
||||
// donc PAS une eau chaude. Regle : 15 min apres le debut de la fenetre, session
|
||||
// engagee, contacteur ferme, ballon sous tension, et la puissance de la
|
||||
// RESISTANCE jamais atteinte depuis le debut de la fenetre -> on le SIGNALE.
|
||||
// Memoire de la fenetre (puissance max vue) dans le helper lui-meme ; le
|
||||
// concierge n'envoie que `active: true`, une fois par `since` (debut de la
|
||||
// fenetre). Limite assumee : ballon deja a 65 °C en BOOST = 0 W aussi, donc un
|
||||
// message pour rien au plus par fenetre (aucune temperature dans HA).
|
||||
const ALERTE_DELAI_MIN = 15;
|
||||
const seuilResistanceW = ((normalized.greenPowerW ?? 620) + (normalized.autoPowerW ?? 1200)) / 2;
|
||||
const minNow = Number.isFinite(now.minNow) ? now.minNow : null;
|
||||
const fenetreHc = (normalized.hcWindows || []).find((w) => minNow !== null && (
|
||||
w.debut <= w.fin ? (minNow >= w.debut && minNow < w.fin) : (minNow >= w.debut || minNow < w.fin)));
|
||||
const depuisDebutMin = fenetreHc ? ((minNow - fenetreHc.debut + 1440) % 1440) : null;
|
||||
const alerteBoost = !!fenetreHc && depuisDebutMin >= ALERTE_DELAI_MIN
|
||||
&& hcSessionCommitted && gridIsOn && waterHeaterSwitchOn && observedRegime === 'hp_low';
|
||||
let alerteChauffeEau = '';
|
||||
if (alerteBoost) {
|
||||
if (fenetreHc) {
|
||||
const depuisDebutMin = (minNow - fenetreHc.debut + 1440) % 1440;
|
||||
const h = String(Math.floor(fenetreHc.debut / 60)).padStart(2, '0') + ':' + String(fenetreHc.debut % 60).padStart(2, '0');
|
||||
const jourFenetre = (fenetreHc.debut > fenetreHc.fin && minNow < fenetreHc.fin)
|
||||
? new Date(nowMs - 86400000).toLocaleDateString('sv-SE', { timeZone: 'Europe/Paris' }) : now.date;
|
||||
const since = `${jourFenetre} ${h}`;
|
||||
let prev = null;
|
||||
try { prev = JSON.parse(input.alerte_prev || ''); } catch { prev = null; }
|
||||
const maxPrev = prev && prev.since === since && Number.isFinite(prev.max_w) ? prev.max_w : 0;
|
||||
const maxW = Math.round(Math.max(maxPrev, waterHeaterSwitchOn ? observedPowerW : 0));
|
||||
const active = depuisDebutMin >= ALERTE_DELAI_MIN && hcSessionCommitted && gridIsOn
|
||||
&& waterHeaterSwitchOn && maxW < seuilResistanceW;
|
||||
alerteChauffeEau = JSON.stringify({
|
||||
active: true, since: `${jourFenetre} ${h}`, fenetre: fenetreHc.nom,
|
||||
puissance_w: Math.round(observedPowerW),
|
||||
message: `🚿 Heures creuses EDF : le chauffe-eau ne tire que ${Math.round(observedPowerW)} W (pompe a chaleur). Passe-le en BOOST pour avoir 65 °C a ${fenetreHc.douche}.`,
|
||||
active, since, fenetre: fenetreHc.nom, max_w: maxW, puissance_w: Math.round(observedPowerW),
|
||||
...(active ? { message: `🚿 Heures creuses EDF : le chauffe-eau ne chauffe pas a pleine puissance (${Math.round(observedPowerW)} W). Si tu n'es pas en BOOST, passe-le pour avoir 65 °C a ${fenetreHc.douche}.` } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"ecs_24h_kwh": states("sensor.evcs_chauffe_eau_24h_kwh"),
|
||||
"ecs_jour_kwh": states("sensor.evcs_chauffe_eau_jour_kwh"),
|
||||
"off_jour_faible_prev": states("input_boolean.victron_water_heater_off_jour_faible"),
|
||||
"alerte_prev": states("input_text.evcs_alerte_chauffe_eau"),
|
||||
"hc_midday_start": states("input_datetime.victron_evcs_hc_midday_start"),
|
||||
"hc_midday_stop": states("input_datetime.victron_evcs_hc_midday_stop"),
|
||||
"hc_night_start": states("input_datetime.victron_evcs_hc_night_start"),
|
||||
|
||||
@@ -53,7 +53,8 @@ eq('helper corrompu repart a vide', JSON.parse(ajouterReleve('pas du json', { ve
|
||||
const journ = async () => {};
|
||||
const appels = [];
|
||||
const etiq = async (b, n, m) => { appels.push([b.toISOString(), n, m]); return 1; };
|
||||
const etat = (km, niv, maj, branchee = 'on', borne = 'on') => new Map([
|
||||
const etat = (km, niv, maj, branchee = 'on', borne = 'on', contact = 'off') => new Map([
|
||||
[JEEP.contact, { state: contact }],
|
||||
[JEEP.odometre, { state: String(km) }], [JEEP.niveau, { state: String(niv) }], [JEEP.majA, { state: maj }],
|
||||
[JEEP.branchee, { state: branchee }], ['binary_sensor.victron_evcs_vehicle_connected', { state: borne }],
|
||||
['input_number.victron_evcs_capacity_jeep_kwh', { state: '8.7' }], ['input_number.victron_evcs_autonomie_jeep_km', { state: '50' }]]);
|
||||
@@ -83,6 +84,11 @@ eq('helper corrompu repart a vide', JSON.parse(ajouterReleve('pas du json', { ve
|
||||
base.branche = new Date('2026-10-09T17:06:00Z'); base.etiquette = 'VW';
|
||||
await ingererJeep(sql, lire, etat(112950, 40, '2026-10-09T17:05:00+00:00', 'off', 'on'), journ, etiq);
|
||||
eq('jeep : proprietaire a deja dit VW -> rien', [appels.length, base.niveaux.length], [2, 2]);
|
||||
// 07/10 : une autre voiture est branchee ; la Jeep publie APRES le branchement
|
||||
// avec le contact mis et se declare « branchee » (cas extreme) -> pas JEEP.
|
||||
base.branche = new Date('2026-10-10T10:32:00Z'); base.etiquette = null;
|
||||
await ingererJeep(sql, lire, etat(113000, 50, '2026-10-10T10:37:20+00:00', 'on', 'on', 'on'), journ, etiq);
|
||||
eq('jeep : contact mis apres le branchement -> pas d identification', appels.length, 2);
|
||||
}
|
||||
|
||||
if (process.argv.includes('--vues')) {
|
||||
|
||||
@@ -81,25 +81,28 @@ for (const c of cas) {
|
||||
if (o.observedPowerW === 0 && o.observedRegime === 'off') { ok++; console.log('✅ coupe-zero-watt'); }
|
||||
else { ko++; console.log(`❌ coupe-zero-watt | ${o.observedPowerW} W ${o.observedRegime}`); }
|
||||
}
|
||||
// Alerte BOOST (07/10) : en HC sur EDF, le ballon ne tire que la pompe a chaleur.
|
||||
// Alerte BOOST (07/10) : en HC sur EDF, la resistance n'a jamais ete atteinte.
|
||||
{
|
||||
const minuit = new Date(new Date().toLocaleDateString('sv-SE', { timeZone: 'Europe/Paris' }) + 'T00:00:00');
|
||||
const nowMin = (() => { const p = new Intl.DateTimeFormat('fr-FR', { timeZone: 'Europe/Paris', hour: '2-digit', minute: '2-digit', hour12: false }).format(new Date()).split(':'); return Number(p[0]) * 60 + Number(p[1]); })();
|
||||
const hm = (m) => { const x = ((m % 1440) + 1440) % 1440; return `${String(Math.floor(x / 60)).padStart(2, '0')}:${String(x % 60).padStart(2, '0')}:00`; };
|
||||
const stamp = new Date(Date.now() - 5 * 60000).toLocaleString('sv-SE', { timeZone: 'Europe/Paris' });
|
||||
const fenetre = { hc_midday_start: hm(nowMin - 30), hc_midday_stop: hm(nowMin + 60), hc_night_start: hm(nowMin + 300), hc_night_stop: hm(nowMin + 600) };
|
||||
const base = { ...BASE, ...fenetre, grid_state: 'on', hc_session_committed: 'on', water_heater_switch: 'on',
|
||||
water_heater_energy_total_kwh: '18.65', energy_total_prev_kwh: '18.60', energy_prev_at: stamp }; // 0,05 kWh / 5 min = 600 W : pompe a chaleur
|
||||
const verif = (nom, snap, attenduAlerte) => {
|
||||
water_heater_energy_total_kwh: '18.65', energy_total_prev_kwh: '18.60', energy_prev_at: stamp }; // 600 W : pompe a chaleur
|
||||
const debut = new Date().toLocaleDateString('sv-SE', { timeZone: 'Europe/Paris' }) + ' ' + hm(nowMin - 30).slice(0, 5);
|
||||
const verif = (nom, snap, attendu) => {
|
||||
const s = executer({ ...base, ...snap });
|
||||
const a = s.ARBITRATION[0].json.finalDecision.alerteChauffeEau;
|
||||
const ok1 = attenduAlerte ? (a && JSON.parse(a).active === true && JSON.parse(a).message.includes('BOOST')) : a === '';
|
||||
let j = null; try { j = JSON.parse(a); } catch {}
|
||||
const actif = !!(j && j.active === true && j.message && j.message.includes('BOOST'));
|
||||
const appel = s.BUILD_ACTIONS.map((x) => x.json).find((c) => c.data?.entity_id === 'input_text.evcs_alerte_chauffe_eau');
|
||||
if (ok1 && appel) { ok++; console.log(`✅ ${nom}`); } else { ko++; console.log(`❌ ${nom} | alerte=${a} appel=${!!appel}`); }
|
||||
if (actif === attendu && appel) { ok++; console.log(`✅ ${nom}`); } else { ko++; console.log(`❌ ${nom} | alerte=${a} appel=${!!appel}`); }
|
||||
};
|
||||
verif('alerte-boost-pompe-a-chaleur-en-hc', {}, true);
|
||||
verif('alerte-boost-auto-arrete-0-w-en-hc', { water_heater_energy_total_kwh: '18.60' }, true);
|
||||
verif('alerte-boost-resistance-pas-d-alerte', { water_heater_energy_total_kwh: '18.75' }, false);
|
||||
verif('alerte-boost-thermostat-satisfait-pas-d-alerte', { water_heater_energy_total_kwh: '18.60' }, false);
|
||||
verif('alerte-boost-resistance-deja-vue-dans-la-fenetre', { water_heater_energy_total_kwh: '18.60',
|
||||
alerte_prev: JSON.stringify({ active: false, since: debut, max_w: 1235 }) }, false);
|
||||
verif('alerte-boost-debut-de-fenetre-attente', { hc_midday_start: hm(nowMin - 5) }, false);
|
||||
verif('alerte-boost-edf-ouvert-pas-d-alerte', { grid_state: 'off' }, false);
|
||||
}
|
||||
|
||||
@@ -470,6 +470,25 @@ async function transitionsCachees(id, depart, depuisIso, jusquaIso) {
|
||||
return transitionsDansSerie(series[0], depart, depuisIso, jusquaIso);
|
||||
}
|
||||
|
||||
// RELECTURE UCONNECT AU BRANCHEMENT (07/10/2026, accord du proprietaire) :
|
||||
// l'integration ne relit le cloud Stellantis que toutes les 15 min ; au
|
||||
// branchement de la borne on demande `uconnect.update`, une RELECTURE du cloud
|
||||
// (ce que la voiture a deja publie : fin de trajet, contact). AUCUNE commande
|
||||
// vers la voiture : deep_refresh / refresh_location la reveillent et restent
|
||||
// interdits, comme charge_now, lights, doors. Une fois par branchement.
|
||||
let derniereRelectureUconnect = 0;
|
||||
async function relireUconnect() {
|
||||
if (DRY || Date.now() - derniereRelectureUconnect < 5 * 60000) return;
|
||||
derniereRelectureUconnect = Date.now();
|
||||
try {
|
||||
const r = await fetch(`${HA_URL}/api/services/uconnect/update`, {
|
||||
method: 'POST', headers: { Authorization: `Bearer ${HA_TOKEN}`, 'Content-Type': 'application/json' },
|
||||
body: '{}', signal: AbortSignal.timeout(20000),
|
||||
});
|
||||
console.log(` relecture Uconnect au branchement : HTTP ${r.status}`);
|
||||
} catch (e) { console.log(` relecture Uconnect au branchement : ${e.message}`); }
|
||||
}
|
||||
|
||||
async function detecterEvenements(sql, st) {
|
||||
if (!memoire.amorce) {
|
||||
for (const [id] of TRANSITIONS_SUIVIES.map((t) => [t[0]])) {
|
||||
@@ -533,6 +552,7 @@ async function detecterEvenements(sql, st) {
|
||||
}, tr.a);
|
||||
n++;
|
||||
}
|
||||
if (id === 'binary_sensor.victron_evcs_vehicle_connected' && cachees.some((x) => x.apres === 'on')) await relireUconnect();
|
||||
continue;
|
||||
}
|
||||
const qui = await auteurDeLaTransition(id, e.last_changed);
|
||||
@@ -551,6 +571,7 @@ async function detecterEvenements(sql, st) {
|
||||
});
|
||||
memoire.valeurs.set(id, cur);
|
||||
n++;
|
||||
if (id === 'binary_sensor.victron_evcs_vehicle_connected' && cur === 'on') await relireUconnect();
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -274,6 +274,7 @@ export const JEEP = {
|
||||
niveau: 'sensor.jeep_renegade_phev_hvbattery_charge',
|
||||
majA: 'sensor.jeep_renegade_phev_last_info_update_at',
|
||||
branchee: 'binary_sensor.jeep_renegade_phev_ev_charger',
|
||||
contact: 'binary_sensor.jeep_renegade_phev_ignition',
|
||||
};
|
||||
|
||||
// `etiqueter(branche, niveau)` (fourni par le service) : pose l'etiquette JEEP
|
||||
@@ -327,6 +328,12 @@ export async function ingererJeep(sql, lire, st, journaliser, etiqueter = null)
|
||||
[new Date(branche.getTime() - 10 * 60000), branche]);
|
||||
if (arr.length) preuve = { ts: arr[0].ts, niveau: Number(arr[0].niveau_pct), motif: 'arrivee' };
|
||||
}
|
||||
// Jeep vue AILLEURS apres le branchement (07/10/2026, accord du proprietaire) :
|
||||
// une information Uconnect posterieure au branchement avec le contact MIS
|
||||
// dit qu'elle roule — ce n'est pas elle sur la borne. (La position ne sert
|
||||
// pas : la Jeep garee a la maison est a 2,2 km de zone.home, donc toujours
|
||||
// « not_home ».)
|
||||
if (preuve && branche && st.get(JEEP.contact)?.state === 'on' && ts > branche) preuve = null;
|
||||
// Le proprietaire a deja nomme un AUTRE vehicule pour ce branchement : on n'y touche pas.
|
||||
let autre = false;
|
||||
if (preuve) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user