Nuit : la voiture profite d'EDF ferme pour la batterie, la nuit prepare le soir ; conseiller IA relecteur de la journee (revue calculee + pistes)
This commit is contained in:
@@ -31,6 +31,7 @@ import { fileURLToPath } from 'node:url';
|
||||
import pg from 'pg';
|
||||
import { creerSchemaKm, ingererReleves, ingererNiveauSaisi, ingererJeep } from './kilometrage.mjs';
|
||||
import { texteRapport, envoyer, donneesNuit, texteDepuis } from './rapport-nuit.mjs';
|
||||
import { donneesRevue, constatsRevue } from './revue-jour.mjs';
|
||||
import { diagnosticPv, etatCellules, productionJour, chargeSolaire, bilanNuit, etatReseau, importJour, minuitLocal, COMPTEURS_IMPORT, COMPTEUR_EDF, consoJour, COMPTEURS_CONSO, projectionSoir, previsionNuit, vrmVersCourbes, nuitMaisonPrevue } from './diagnostics-agent.mjs';
|
||||
import { transitionsDansSerie } from './allers-retours.mjs';
|
||||
import { energieSur24h, energieDepuis, COMPTEUR_CHAUFFE_EAU } from './chauffe-eau.mjs';
|
||||
@@ -1122,6 +1123,83 @@ async function envoyerRapportDuMatin(sql, client) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// REVUE DE LA JOURNEE (08/10/2026, tools/revue-jour.mjs) : a 07:30, les
|
||||
// constats (calcules, testes) sont publies dans sensor.evcs_revue_jour ; le
|
||||
// conseiller IA les relit vers 07:35 et publie ses pistes dans
|
||||
// sensor.evcs_advisor_revue ; a 07:45 le courriel « revue de la journee » part
|
||||
// avec les constats et, s'il a repondu (sinon a 08:15 sans lui), ses pistes —
|
||||
// presentees comme un AVIS, jamais comme un fait.
|
||||
const REVUE_PUBLIEE = 7 * 60 + 30;
|
||||
const REVUE_COURRIEL = 7 * 60 + 45;
|
||||
const REVUE_COURRIEL_SANS_AVIS = 8 * 60 + 15;
|
||||
function partsParis() {
|
||||
const p = Object.fromEntries(new Intl.DateTimeFormat('fr-FR', {
|
||||
timeZone: 'Europe/Paris', year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', hour12: false,
|
||||
}).formatToParts(new Date()).filter((x) => x.type !== 'literal').map((x) => [x.type, Number(x.value)]));
|
||||
return { min: p.hour * 60 + p.minute, jour: `${p.year}-${String(p.month).padStart(2, '0')}-${String(p.day).padStart(2, '0')}` };
|
||||
}
|
||||
async function dejaFait(client, genre, jour) {
|
||||
const { rowCount } = await client.query(`SELECT 1 FROM evenement WHERE genre = $1
|
||||
AND ts >= $2::date AND ts < $2::date + interval '1 day' LIMIT 1`, [genre, jour]);
|
||||
return rowCount > 0;
|
||||
}
|
||||
async function revueDeLaJournee(sql, client, st) {
|
||||
if (!client) return 0;
|
||||
const { min, jour } = partsParis();
|
||||
if (min < REVUE_PUBLIEE || min > 10 * 60) return 0;
|
||||
const hc = (id) => (st.get(id)?.state || '').slice(0, 5);
|
||||
const fenetres = [
|
||||
{ nom: 'nuit', debut: hc('input_datetime.victron_evcs_hc_night_start'), fin: hc('input_datetime.victron_evcs_hc_night_stop') },
|
||||
{ nom: 'midi', debut: hc('input_datetime.victron_evcs_hc_midday_start'), fin: hc('input_datetime.victron_evcs_hc_midday_stop') },
|
||||
];
|
||||
let n = 0;
|
||||
if (!(await dejaFait(client, 'revue', jour))) {
|
||||
// Fin de la fenetre = fin des HC de nuit d'aujourd'hui + 1 min.
|
||||
const fin = Date.parse(`${jour}T${fenetres[0].fin || '07:24'}:00${decalageParis(jour)}`) + 60000;
|
||||
const r = constatsRevue(await donneesRevue(client, fenetres, fin));
|
||||
const aExaminer = r.constats.filter((c) => c.gravite === 'a_examiner').length;
|
||||
await publierCapteur('sensor.evcs_revue_jour', jour, {
|
||||
friendly_name: 'Revue de la journee',
|
||||
a_examiner: aExaminer,
|
||||
constats: r.constats.map((c) => ({ code: c.code, gravite: c.gravite, texte: c.texte, contexte: c.contexte })),
|
||||
texte: r.texte,
|
||||
});
|
||||
await journaliser(sql, 'revue', `Revue de la journee : ${aExaminer} point(s) a examiner`, { jour, texte: r.texte });
|
||||
n++;
|
||||
}
|
||||
if (min >= REVUE_COURRIEL && !(await dejaFait(client, 'revue_courriel', jour))) {
|
||||
const revue = st.get('sensor.evcs_revue_jour');
|
||||
const avis = st.get('sensor.evcs_advisor_revue');
|
||||
const avisDuJour = avis && avis.state === jour ? avis.attributes : null;
|
||||
if (revue && revue.state === jour && (avisDuJour || min >= REVUE_COURRIEL_SANS_AVIS)) {
|
||||
const a = revue.attributes || {};
|
||||
const pistes = (avisDuJour?.pistes || []).map((p) => `- ${p.piste}`);
|
||||
const texte = [
|
||||
`Revue de la journee (24 h jusqu'a la fin des heures creuses de ce matin)`,
|
||||
'',
|
||||
'CONSTATS (mesures) :',
|
||||
a.texte || '(aucun)',
|
||||
'',
|
||||
a.a_examiner > 0
|
||||
? (pistes.length ? ['PISTES DU CONSEILLER IA (avis a verifier, pas des faits) :', ...pistes].join('\n')
|
||||
: `Pistes du conseiller IA : ${avisDuJour ? 'aucune' : 'pas de reponse ce matin'}.`)
|
||||
: '',
|
||||
].join('\n').trim();
|
||||
await envoyer(texte, `EVCS - revue de la journee${a.a_examiner > 0 ? ` (${a.a_examiner} point(s))` : ''}`);
|
||||
await journaliser(sql, 'revue_courriel', 'Revue de la journee envoyee par courriel', { jour, avis: !!avisDuJour });
|
||||
n++;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
function decalageParis(jour) {
|
||||
const d = new Date(`${jour}T12:00:00Z`);
|
||||
const h = Number(new Intl.DateTimeFormat('en-GB', { timeZone: 'Europe/Paris', hour: '2-digit', hour12: false }).format(d));
|
||||
return `+${String(h - 12).padStart(2, '0')}:00`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Verse dans la base les relevés de cellule deposes par le poste qui porte le
|
||||
// cable serie. Celui-ci n'ecrit qu'un CSV dans le depot partage : il n'a donc
|
||||
@@ -2355,6 +2433,9 @@ async function cycle(client) {
|
||||
rapport: DRY ? 0 : await envoyerRapportDuMatin(sql, client).catch((e) => {
|
||||
console.error('rapport de nuit :', e.message); return 0;
|
||||
}),
|
||||
revue: DRY ? 0 : await revueDeLaJournee(sql, client, st).catch((e) => {
|
||||
console.error('revue de la journee :', e.message); return 0;
|
||||
}),
|
||||
cellules: DRY ? 0 : await ingererCellules(client).catch((e) => {
|
||||
console.error(`ingestion cellules en echec : ${e.message}`);
|
||||
return 0;
|
||||
|
||||
16
tools/patch-20261008-advisor-relecteur.mjs
Normal file
16
tools/patch-20261008-advisor-relecteur.mjs
Normal file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
// 08/10/2026 (proprietaire) : le conseiller devient RELECTEUR de la journee. Son
|
||||
// noeud d'ecriture doit pouvoir publier un capteur avec attributs
|
||||
// (sensor.evcs_advisor_revue) : un item `state_entity` part vers /api/states,
|
||||
// les autres vers /api/services comme avant. Rejouable (idempotent).
|
||||
import fs from 'fs';
|
||||
const F = 'workflows/evcs-advisor-shadow-v1.json';
|
||||
const wf = JSON.parse(fs.readFileSync(F, 'utf8'));
|
||||
const n = wf.nodes.find((x) => x.name === 'Execute HA Service Advisor');
|
||||
const ancien = "={{ 'http://10.10.10.17:8123/api/services/' + $json.domain + '/' + $json.service }}";
|
||||
const nouveau = "={{ $json.state_entity ? 'http://10.10.10.17:8123/api/states/' + $json.state_entity : 'http://10.10.10.17:8123/api/services/' + $json.domain + '/' + $json.service }}";
|
||||
if (n.parameters.url === nouveau) { console.log('deja applique'); process.exit(0); }
|
||||
if (n.parameters.url !== ancien) throw new Error('url inattendue : ' + n.parameters.url);
|
||||
n.parameters.url = nouveau;
|
||||
fs.writeFileSync(F, JSON.stringify(wf, null, 2) + '\n');
|
||||
console.log('patch applique');
|
||||
218
tools/revue-jour.mjs
Normal file
218
tools/revue-jour.mjs
Normal file
@@ -0,0 +1,218 @@
|
||||
// =====================================================================
|
||||
// REVUE DE LA JOURNEE (08/10/2026, decision du proprietaire)
|
||||
//
|
||||
// Chaque matin, ce que le systeme a DECIDE est confronte a ce qui s'est PASSE,
|
||||
// sur les 24 h qui finissent avec la fenetre HC de nuit. Nuit du 07-08/10 : la
|
||||
// HC de midi avait ete perdue, EDF s'etait ferme en heures pleines a 18:03, et
|
||||
// la VW etait restee bloquee de 01:24 a 05:14 alors qu'EDF etait ferme pour la
|
||||
// batterie — rien de cela n'avait ete signale par aucun automate.
|
||||
//
|
||||
// ⚠️ DOCTRINE (§79) : les CONSTATS sont calcules ici, par du code teste. Le
|
||||
// conseiller IA ne fait que les RELIRE (pistes a examiner) ; il ne produit
|
||||
// aucun chiffre et ne decide rien. Un constat ne depend jamais de lui.
|
||||
//
|
||||
// `constatsRevue` est pure (tests : tests/revue-jour/run.mjs) ;
|
||||
// `donneesRevue` fait les lectures en base.
|
||||
// =====================================================================
|
||||
|
||||
const MIN = 60000;
|
||||
const KWH_PAR_POINT = 0.30; // courbe batterie mesuree le 20/09 (HANDOFF §74)
|
||||
const SEUIL_HP_MIN = 5; // fermeture EDF en heures pleines retenue au-dela
|
||||
const SEUIL_RESEAU_W = 100; // bruit de mesure du reseau (comme sensor.evcs_pv_diagnostic)
|
||||
const SEUIL_VOITURE_MIN = 10; // voiture bloquee pendant une session HC
|
||||
const SEUIL_OBJECTIF_PTS = 2; // batterie sous son objectif en fin de HC
|
||||
|
||||
const ETATS_VOITURE_ABSENTE_OU_PLEINE = /not_connected|done|full|complete|charged/i;
|
||||
|
||||
function minutesParis(ms) {
|
||||
const p = new Intl.DateTimeFormat('fr-FR', { timeZone: 'Europe/Paris', hour: '2-digit', minute: '2-digit', hour12: false })
|
||||
.formatToParts(new Date(ms));
|
||||
return Number(p.find((x) => x.type === 'hour').value) % 24 * 60 + Number(p.find((x) => x.type === 'minute').value);
|
||||
}
|
||||
export function hhmm(ms) {
|
||||
return new Date(ms).toLocaleTimeString('fr-FR', { timeZone: 'Europe/Paris', hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
const hm = (s) => { const [h, m] = String(s || '').split(':').map(Number); return Number.isFinite(h) && Number.isFinite(m) ? h * 60 + m : null; };
|
||||
const f1 = (x) => String(Math.round(x * 10) / 10).replace('.', ',');
|
||||
const duree = (min) => (min >= 60 ? `${Math.floor(min / 60)} h ${String(Math.round(min % 60)).padStart(2, '0')}` : `${Math.round(min)} min`);
|
||||
|
||||
// fenetres : [{ nom: 'nuit'|'midi', debut: 'HH:MM', fin: 'HH:MM' }]
|
||||
export function enHc(ms, fenetres) {
|
||||
const m = minutesParis(ms);
|
||||
return fenetres.some((f) => {
|
||||
const a = hm(f.debut), b = hm(f.fin);
|
||||
if (a === null || b === null) return false;
|
||||
return a <= b ? m >= a && m < b : m >= a || m < b;
|
||||
});
|
||||
}
|
||||
|
||||
// Valeur d'une serie en escalier (derniere valeur connue a l'instant t).
|
||||
function aInstant(serie, t) {
|
||||
let v = null;
|
||||
for (const p of serie) { if (p.ts <= t) v = p.v; else break; }
|
||||
return v;
|
||||
}
|
||||
|
||||
// Plages contigues ou `pred(t)` est vrai, echantillonnees a la minute.
|
||||
function plages(debut, fin, pred) {
|
||||
const out = [];
|
||||
let cur = null;
|
||||
for (let t = debut; t < fin; t += MIN) {
|
||||
if (pred(t)) { if (!cur) cur = { debut: t, fin: t + MIN }; else cur.fin = t + MIN; }
|
||||
else if (cur) { out.push(cur); cur = null; }
|
||||
}
|
||||
if (cur) out.push(cur);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} d
|
||||
* debut, fin : ms
|
||||
* fenetres : fenetres HC (voir enHc)
|
||||
* contacteur : [{ ts, v: 'on'|'off', auteur }] trie, avec l'etat avant `debut` en tete
|
||||
* edfKwh : [{ ts, v }] compteur EDF cumule (kWh)
|
||||
* reseauW : [{ ts, v }] sensor.victron_grid_power_l1 (W)
|
||||
* decisions : [{ ts, profil, source_mode, etat, session_engagee, soc_pct, cible_matin_pct, raison }]
|
||||
* soc : [{ ts, v }]
|
||||
* requis : [{ ts, v }] input_number.evcs_balance_required_soc
|
||||
*/
|
||||
export function constatsRevue(d) {
|
||||
const constats = [];
|
||||
const contact = d.contacteur.map((c) => ({ ts: c.ts, v: c.v }));
|
||||
|
||||
// 1. EDF ferme en HEURES PLEINES. Juge sur la PUISSANCE RESEAU mesuree, pas
|
||||
// sur le journal du contacteur : le 07/10 l'ouverture de 07:24 n'y figurait
|
||||
// pas, et le journal seul annoncait 5 h d'EDF pour 0,2 kWh reellement achete.
|
||||
// Les plages separees de moins de 5 min sont fusionnees (un ecart bref de la
|
||||
// mesure ne coupe pas une fermeture).
|
||||
const brutes = plages(d.debut, d.fin, (t) => (aInstant(d.reseauW, t) ?? 0) > SEUIL_RESEAU_W && !enHc(t, d.fenetres));
|
||||
const fusion = [];
|
||||
for (const p of brutes) {
|
||||
const der = fusion[fusion.length - 1];
|
||||
if (der && p.debut - der.fin <= SEUIL_HP_MIN * MIN) der.fin = p.fin; else fusion.push({ ...p });
|
||||
}
|
||||
for (const p of fusion) {
|
||||
const min = (p.fin - p.debut) / MIN;
|
||||
if (min < SEUIL_HP_MIN) continue;
|
||||
const k0 = aInstant(d.edfKwh, p.debut), k1 = aInstant(d.edfKwh, p.fin);
|
||||
const kwh = k0 !== null && k1 !== null && k1 >= k0 ? k1 - k0 : null;
|
||||
const ouverture = [...d.contacteur].reverse().find((c) => c.v === 'on' && c.ts <= p.debut + 2 * MIN && c.ts >= p.debut - 30 * MIN);
|
||||
const socDebut = aInstant(d.soc, p.debut);
|
||||
constats.push({
|
||||
code: 'edf_heures_pleines',
|
||||
gravite: 'a_examiner',
|
||||
debut: p.debut, fin: p.fin,
|
||||
texte: `EDF ferme en heures pleines de ${hhmm(p.debut)} a ${hhmm(p.fin)} (${duree(min)})`
|
||||
+ (kwh !== null ? `, ${f1(kwh)} kWh achetes` : '')
|
||||
+ (socDebut !== null ? `, batterie a ${Math.round(socDebut)} % a la fermeture` : '')
|
||||
+ (ouverture?.auteur ? ` ; ferme par ${ouverture.auteur}` : '') + '.',
|
||||
chiffres: { minutes: Math.round(min), kwh: kwh === null ? null : Math.round(kwh * 100) / 100, soc: socDebut },
|
||||
contexte: (aInstant(d.decisions.map((x) => ({ ts: x.ts, v: x.raison })), p.debut) || '').slice(0, 400),
|
||||
});
|
||||
}
|
||||
|
||||
// 2. VOITURE BRANCHEE BLOQUEE pendant une session reseau HC (EDF ferme)
|
||||
const dec = (t) => { let r = null; for (const x of d.decisions) { if (x.ts <= t) r = x; else break; } return r; };
|
||||
for (const p of plages(d.debut, d.fin, (t) => {
|
||||
const x = dec(t);
|
||||
return !!x && enHc(t, d.fenetres) && aInstant(contact, t) === 'on' && x.session_engagee === true
|
||||
&& /bloqu/i.test(x.source_mode || '') && !ETATS_VOITURE_ABSENTE_OU_PLEINE.test(x.etat || '');
|
||||
})) {
|
||||
const min = (p.fin - p.debut) / MIN;
|
||||
if (min < SEUIL_VOITURE_MIN) continue;
|
||||
const x = dec(p.debut);
|
||||
constats.push({
|
||||
code: 'voiture_bloquee_session_hc',
|
||||
gravite: 'a_examiner',
|
||||
debut: p.debut, fin: p.fin,
|
||||
texte: `Voiture branchee mais bloquee de ${hhmm(p.debut)} a ${hhmm(p.fin)} (${duree(min)}) alors qu'EDF etait ferme en heures creuses (etat ${x.etat}).`,
|
||||
chiffres: { minutes: Math.round(min) },
|
||||
contexte: (x.raison || '').slice(0, 400),
|
||||
});
|
||||
}
|
||||
|
||||
// 3. BATTERIE SOUS SON OBJECTIF a la fin d'une fenetre HC ou une session etait engagee
|
||||
for (const f of d.fenetres) {
|
||||
const b = hm(f.fin);
|
||||
if (b === null) continue;
|
||||
for (let t = d.debut; t <= d.fin; t += MIN) {
|
||||
if (minutesParis(t) !== b) continue;
|
||||
const x = dec(t - MIN);
|
||||
if (!x || x.session_engagee !== true) continue;
|
||||
const objectif = f.nom === 'nuit' ? Number(x.cible_matin_pct) : aInstant(d.requis, t - 5 * MIN);
|
||||
const soc = aInstant(d.soc, t);
|
||||
if (!Number.isFinite(objectif) || !Number.isFinite(soc)) continue;
|
||||
// Jour faible a midi (08/10) : le bilan vise 100 % = « charge au maximum
|
||||
// possible ». Le manque a 100 % ne dit rien ; on rend compte du gain.
|
||||
if (f.nom === 'midi' && objectif >= 99.5) {
|
||||
const a = hm(f.debut);
|
||||
let t0 = null;
|
||||
for (let u = t - 4 * 60 * MIN; u < t; u += MIN) if (minutesParis(u) === a) { t0 = u; break; }
|
||||
const soc0 = t0 === null ? null : aInstant(d.soc, t0);
|
||||
constats.push({
|
||||
code: 'charge_max_midi',
|
||||
gravite: 'info',
|
||||
debut: t, fin: t,
|
||||
texte: `Heures creuses de midi en charge maximale (jour faible) : batterie ${soc0 === null ? '?' : Math.round(soc0)} % -> ${Math.round(soc)} %`
|
||||
+ (soc0 === null ? '.' : ` (+${f1((soc - soc0) * KWH_PAR_POINT)} kWh).`),
|
||||
chiffres: { socDebut: soc0, socFin: soc },
|
||||
contexte: (x.raison || '').slice(0, 400),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const manque = objectif - soc;
|
||||
if (manque < SEUIL_OBJECTIF_PTS) continue;
|
||||
constats.push({
|
||||
code: `batterie_sous_objectif_${f.nom}`,
|
||||
gravite: 'a_examiner',
|
||||
debut: t, fin: t,
|
||||
texte: `Fin des heures creuses de ${f.nom} (${hhmm(t)}) : batterie a ${Math.round(soc)} % pour ${f1(objectif)} % vises, il manquait ${f1(manque * KWH_PAR_POINT)} kWh.`,
|
||||
chiffres: { soc, objectif, manqueKwh: Math.round(manque * KWH_PAR_POINT * 10) / 10 },
|
||||
contexte: (x.raison || '').slice(0, 400),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
constats.sort((a, b) => a.debut - b.debut);
|
||||
const texte = constats.length
|
||||
? constats.map((c) => `- ${c.texte}`).join('\n')
|
||||
: 'Rien a signaler : pas d\'EDF en heures pleines, pas de voiture bloquee pendant une session, batterie a son objectif en fin d\'heures creuses.';
|
||||
return { constats, texte };
|
||||
}
|
||||
|
||||
export async function donneesRevue(client, fenetres, finMs) {
|
||||
// 24 h qui se terminent a la fin de la fenetre HC de nuit (07:24 + 1 min).
|
||||
const fin = finMs;
|
||||
const debut = fin - 24 * 60 * MIN;
|
||||
const q = async (sql, v) => (await client.query(sql, v)).rows;
|
||||
const d0 = new Date(debut), d1 = new Date(fin);
|
||||
const ms = (r) => new Date(r.ts).getTime();
|
||||
|
||||
const avant = await q(`SELECT ts, detail->>'apres' v, detail->>'auteur_nom' auteur_nom, detail->>'auteur' auteur FROM evenement
|
||||
WHERE genre='reseau' AND resume LIKE 'Contacteur EDF%' AND ts < $1 ORDER BY ts DESC LIMIT 1`, [d0]);
|
||||
const bascules = await q(`SELECT ts, detail->>'apres' v, detail->>'auteur_nom' auteur_nom, detail->>'auteur' auteur, resume FROM evenement
|
||||
WHERE genre='reseau' AND resume LIKE 'Contacteur EDF%' AND ts >= $1 AND ts < $2 ORDER BY ts`, [d0, d1]);
|
||||
const auteurDe = (r) => {
|
||||
const m = /— par (.+)$/.exec(r.resume || '');
|
||||
return r.auteur_nom || (m ? m[1] : null) || r.auteur || null;
|
||||
};
|
||||
const contacteur = [
|
||||
...avant.map((r) => ({ ts: debut, v: r.v, auteur: auteurDe(r) })),
|
||||
...bascules.map((r) => ({ ts: ms(r), v: r.v, auteur: auteurDe(r) })),
|
||||
];
|
||||
const serie = async (cle) => (await q(`SELECT ts, valeur v FROM (
|
||||
(SELECT ts, valeur FROM mesure WHERE cle=$1 AND ts < $2 ORDER BY ts DESC LIMIT 1)
|
||||
UNION ALL (SELECT ts, valeur FROM mesure WHERE cle=$1 AND ts >= $2 AND ts < $3)) s ORDER BY ts`, [cle, d0, d1]))
|
||||
.map((r) => ({ ts: ms(r), v: Number(r.v) }));
|
||||
const decisions = (await q(`SELECT * FROM (
|
||||
(SELECT ts, profil, source_mode, etat, session_engagee, soc_pct, cible_matin_pct, raison FROM decision WHERE ts < $1 ORDER BY ts DESC LIMIT 1)
|
||||
UNION ALL (SELECT ts, profil, source_mode, etat, session_engagee, soc_pct, cible_matin_pct, raison FROM decision WHERE ts >= $1 AND ts < $2)) s ORDER BY ts`, [d0, d1]))
|
||||
.map((r) => ({ ...r, ts: ms(r) }));
|
||||
return {
|
||||
debut, fin, fenetres, contacteur, decisions,
|
||||
edfKwh: await serie('sensor.edf_general_ok_energie_totale'),
|
||||
reseauW: await serie('sensor.victron_grid_power_l1'),
|
||||
soc: await serie('sensor.victron_battery_soc'),
|
||||
requis: await serie('input_number.evcs_balance_required_soc'),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user