sensor.evcs_import_jour_kwh : energie importee d'EDF depuis minuit (le « Depuis reseau » de VRM)
Le proprietaire veut que l'agent vocal lui evite d'ouvrir VRM. « Combien ai-je importe » est une quantite : delta des compteurs cumules AC-in du MultiPlus (vers ac_out + vers inverter) depuis leur valeur a minuit, lue une fois par jour dans l'historique HA. Compteur EDF publie en recoupement. Remise a zero -> indetermine, jamais d'import negatif. Les trois compteurs entrent aussi dans la table mesure (manque note au HANDOFF §76). Tests diagnostics 48 -> 54, deux mutations qui mordent. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011xd7aRDEChdShwsECohnDN
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
// Les cas PV reprennent les gammes du banc OpenClaw (HANDOFF §77) : G3, G4, G5, G7.
|
// Les cas PV reprennent les gammes du banc OpenClaw (HANDOFF §77) : G3, G4, G5, G7.
|
||||||
// node tests/diagnostics-agent/run.mjs
|
// node tests/diagnostics-agent/run.mjs
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import { diagnosticPv, etatCellules, productionJour, chargeSolaire, bilanNuit, etatReseau, MPPT_MODES, ORIGINE_PAR_SOURCE } from '../../tools/diagnostics-agent.mjs';
|
import { diagnosticPv, etatCellules, productionJour, chargeSolaire, bilanNuit, etatReseau, importJour, COMPTEURS_IMPORT, COMPTEUR_EDF, MPPT_MODES, ORIGINE_PAR_SOURCE } from '../../tools/diagnostics-agent.mjs';
|
||||||
|
|
||||||
const LIM = 'voltage_current_limited', ACT = 'mppt_active';
|
const LIM = 'voltage_current_limited', ACT = 'mppt_active';
|
||||||
function st({ pv, conso, reseau, soc = 80, courant = 0, edf = 'off', modes }) {
|
function st({ pv, conso, reseau, soc = 80, courant = 0, edf = 'off', modes }) {
|
||||||
@@ -223,6 +223,31 @@ for (const [nom, r, test] of res) {
|
|||||||
console.log(`${ok ? 'OK ' : 'KO '} ${nom}`);
|
console.log(`${ok ? 'OK ' : 'KO '} ${nom}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const n = cas.length + cel.length + prod.length + charge.length + 1 + bn.length + res.length;
|
// Import depuis minuit. Valeurs de minuit fictives, deltas du 26/09 (0,8 kWh VRM).
|
||||||
|
const [OUT, INV] = COMPTEURS_IMPORT;
|
||||||
|
const mn = { [OUT]: 1200.00, [INV]: 845.00, [COMPTEUR_EDF]: 3082.00 };
|
||||||
|
const imp = [
|
||||||
|
['26/09 : 0,5 vers maison + 0,3 vers batterie -> 0,8 complet, recoupe EDF',
|
||||||
|
importJour({ [OUT]: 1200.5, [INV]: 845.3, [COMPTEUR_EDF]: 3082.83 }, mn),
|
||||||
|
(r) => r.etat === 0.8 && r.attributs.statut === 'complet' && r.attributs.compteur_edf_kwh === 0.8
|
||||||
|
&& r.attributs.detail_kwh.vers_ac_out === 0.5 && r.attributs.detail_kwh.vers_inverter === 0.3],
|
||||||
|
['journee sans reseau -> 0, pas indisponible',
|
||||||
|
importJour({ [OUT]: 1200, [INV]: 845, [COMPTEUR_EDF]: 3082 }, mn), (r) => r.etat === 0 && r.attributs.statut === 'complet'],
|
||||||
|
['compteur remis a zero -> indetermine, jamais un import negatif',
|
||||||
|
importJour({ [OUT]: 3.2, [INV]: 845.3 }, mn), (r) => r.etat === 'indetermine'],
|
||||||
|
['un compteur illisible -> partiel, au moins',
|
||||||
|
importJour({ [OUT]: 1200.5, [INV]: null }, mn), (r) => r.etat === 0.5 && r.attributs.statut === 'partiel'],
|
||||||
|
['valeur de minuit inconnue -> indisponible',
|
||||||
|
importJour({ [OUT]: 1200.5, [INV]: 845.3 }, {}), (r) => r.etat === 'indisponible'],
|
||||||
|
['compteur EDF absent : le total ne change pas, recoupement null',
|
||||||
|
importJour({ [OUT]: 1200.5, [INV]: 845.3 }, mn), (r) => r.etat === 0.8 && r.attributs.compteur_edf_kwh === null],
|
||||||
|
];
|
||||||
|
for (const [nom, r, test] of imp) {
|
||||||
|
const ok = test(r);
|
||||||
|
if (!ok) { ko++; console.log(JSON.stringify(r)); }
|
||||||
|
console.log(`${ok ? 'OK ' : 'KO '} ${nom}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const n = cas.length + cel.length + prod.length + charge.length + 1 + bn.length + res.length + imp.length;
|
||||||
console.log(`\n${n - ko}/${n}`);
|
console.log(`\n${n - ko}/${n}`);
|
||||||
process.exit(ko ? 1 : 0);
|
process.exit(ko ? 1 : 0);
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ export const RENDEMENTS_JOUR = [
|
|||||||
['v1', 'sensor.v1_yield_today'],
|
['v1', 'sensor.v1_yield_today'],
|
||||||
];
|
];
|
||||||
|
|
||||||
function minuitLocal(maintenant) {
|
export function minuitLocal(maintenant) {
|
||||||
const jour = new Intl.DateTimeFormat('en-CA', { timeZone: 'Europe/Paris' }).format(maintenant);
|
const jour = new Intl.DateTimeFormat('en-CA', { timeZone: 'Europe/Paris' }).format(maintenant);
|
||||||
// Heure de Paris a minuit exprimee en UTC : on cherche le decalage du jour.
|
// Heure de Paris a minuit exprimee en UTC : on cherche le decalage du jour.
|
||||||
const decalage = new Date(maintenant.toLocaleString('en-US', { timeZone: 'Europe/Paris' })) -
|
const decalage = new Date(maintenant.toLocaleString('en-US', { timeZone: 'Europe/Paris' })) -
|
||||||
@@ -410,3 +410,59 @@ export function etatReseau(st, maintenant = new Date()) {
|
|||||||
return verdict('raccorde',
|
return verdict('raccorde',
|
||||||
`Reseau EDF raccorde depuis ${attributs.depuis_heure ?? '?'}, import ${attributs.import_w} W.`);
|
`Reseau EDF raccorde depuis ${attributs.depuis_heure ?? '?'}, import ${attributs.import_w} W.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Energie importee d'EDF depuis minuit — le chiffre « Depuis reseau » de VRM
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Demande du proprietaire le 26/09 : l'agent vocal doit lui eviter d'ouvrir VRM.
|
||||||
|
// La question « combien ai-je importe » est une QUANTITE ; la puissance
|
||||||
|
// instantanee (victron_grid_power_l1) n'y repond pas, elle vaut 0 des que le
|
||||||
|
// contacteur est ouvert.
|
||||||
|
// Source : les compteurs cumules AC-in du MultiPlus, sains et monotones (HANDOFF
|
||||||
|
// §76 : 1,60 kWh la nuit du 24/09, recoupes par le compteur EDF a 1,53). Import
|
||||||
|
// = vers la maison (ac_out) + vers la batterie (inverter). Le compteur du
|
||||||
|
// contacteur EDF est publie a cote comme recoupement, pas additionne.
|
||||||
|
// ⚠️ Compteurs sans last_reset : une valeur courante INFERIEURE a celle de
|
||||||
|
// minuit est une remise a zero, pas un import negatif -> indetermine.
|
||||||
|
export const COMPTEURS_IMPORT = [
|
||||||
|
'sensor.airtria_mpii_v1_energy_from_ac_in_1_to_ac_out',
|
||||||
|
'sensor.airtria_mpii_v1_energy_from_ac_in_1_to_inverter',
|
||||||
|
];
|
||||||
|
export const COMPTEUR_EDF = 'sensor.edf_general_ok_energie_totale';
|
||||||
|
|
||||||
|
// courant, minuit : { [entity_id]: nombre | null }
|
||||||
|
export function importJour(courant, minuit) {
|
||||||
|
const detail = {};
|
||||||
|
const muets = [];
|
||||||
|
let total = 0;
|
||||||
|
for (const id of COMPTEURS_IMPORT) {
|
||||||
|
const c = courant[id], m = minuit[id];
|
||||||
|
const court = id.replace('sensor.airtria_mpii_v1_energy_from_ac_in_1_to_', 'vers_');
|
||||||
|
if (!Number.isFinite(c) || !Number.isFinite(m)) { muets.push(court); detail[court] = null; continue; }
|
||||||
|
if (c < m) {
|
||||||
|
return { etat: 'indetermine', attributs: { statut: 'indetermine',
|
||||||
|
explication: `Compteur ${court} remis a zero depuis minuit : import du jour inconnu.` } };
|
||||||
|
}
|
||||||
|
detail[court] = Math.round((c - m) * 100) / 100;
|
||||||
|
total += c - m;
|
||||||
|
}
|
||||||
|
if (muets.length === COMPTEURS_IMPORT.length) {
|
||||||
|
return { etat: 'indisponible', attributs: { statut: 'indisponible',
|
||||||
|
explication: 'Compteurs d\'energie AC-in illisibles : import du jour inconnu.' } };
|
||||||
|
}
|
||||||
|
total = Math.round(total * 10) / 10;
|
||||||
|
const ce = courant[COMPTEUR_EDF], me = minuit[COMPTEUR_EDF];
|
||||||
|
const edf = Number.isFinite(ce) && Number.isFinite(me) && ce >= me ? Math.round((ce - me) * 10) / 10 : null;
|
||||||
|
const partiel = muets.length > 0;
|
||||||
|
return {
|
||||||
|
etat: total,
|
||||||
|
attributs: {
|
||||||
|
statut: partiel ? 'partiel' : 'complet',
|
||||||
|
detail_kwh: detail,
|
||||||
|
compteur_edf_kwh: edf,
|
||||||
|
explication: partiel
|
||||||
|
? `Au moins ${total} kWh importes d'EDF depuis minuit (compteur ${muets.join(', ')} muet).`
|
||||||
|
: `${total} kWh importes d'EDF depuis minuit.`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import path from 'node:path';
|
|||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import pg from 'pg';
|
import pg from 'pg';
|
||||||
import { texteRapport, envoyer, donneesNuit, texteDepuis } from './rapport-nuit.mjs';
|
import { texteRapport, envoyer, donneesNuit, texteDepuis } from './rapport-nuit.mjs';
|
||||||
import { diagnosticPv, etatCellules, productionJour, chargeSolaire, bilanNuit, etatReseau } from './diagnostics-agent.mjs';
|
import { diagnosticPv, etatCellules, productionJour, chargeSolaire, bilanNuit, etatReseau, importJour, minuitLocal, COMPTEURS_IMPORT, COMPTEUR_EDF } from './diagnostics-agent.mjs';
|
||||||
|
|
||||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
const ONCE = process.argv.includes('--once');
|
const ONCE = process.argv.includes('--once');
|
||||||
@@ -83,6 +83,11 @@ const MESURES = [
|
|||||||
'input_number.evcs_balance_required_soc',
|
'input_number.evcs_balance_required_soc',
|
||||||
'input_number.evcs_balance_energy_to_cover_kwh',
|
'input_number.evcs_balance_energy_to_cover_kwh',
|
||||||
'input_number.victron_evcs_night_target_soc',
|
'input_number.victron_evcs_night_target_soc',
|
||||||
|
// Compteurs d'energie cumules de l'import (26/09) : HANDOFF §76 notait leur
|
||||||
|
// absence, qui empechait tout calcul d'import par delta de compteur.
|
||||||
|
'sensor.airtria_mpii_v1_energy_from_ac_in_1_to_ac_out',
|
||||||
|
'sensor.airtria_mpii_v1_energy_from_ac_in_1_to_inverter',
|
||||||
|
'sensor.edf_general_ok_energie_totale',
|
||||||
];
|
];
|
||||||
|
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
@@ -1853,6 +1858,10 @@ async function publierDiagnosticPv(st) {
|
|||||||
await publierCapteur('sensor.evcs_charge_solaire', ch.etat, {
|
await publierCapteur('sensor.evcs_charge_solaire', ch.etat, {
|
||||||
friendly_name: 'EVCS — charge voiture (decision du controleur)', icon: 'mdi:ev-station', ...ch.attributs,
|
friendly_name: 'EVCS — charge voiture (decision du controleur)', icon: 'mdi:ev-station', ...ch.attributs,
|
||||||
});
|
});
|
||||||
|
// Import EDF depuis minuit (« Depuis reseau » de VRM), sur les compteurs cumules.
|
||||||
|
await publierImportJour(st).catch((e) => {
|
||||||
|
console.error(`import du jour : ${e.message}`); return 0;
|
||||||
|
});
|
||||||
// Raccordement reseau : contacteur ouvert = import nul, pas une panne (26/09).
|
// Raccordement reseau : contacteur ouvert = import nul, pas une panne (26/09).
|
||||||
const rr = etatReseau(st);
|
const rr = etatReseau(st);
|
||||||
await publierCapteur('sensor.evcs_reseau', rr.etat, {
|
await publierCapteur('sensor.evcs_reseau', rr.etat, {
|
||||||
@@ -1861,6 +1870,51 @@ async function publierDiagnosticPv(st) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Valeur des compteurs d'energie a minuit (heure de Paris), lue UNE fois par jour
|
||||||
|
// dans l'historique de HA : la derniere valeur connue avant minuit. L'historique
|
||||||
|
// renvoie en tete l'etat en vigueur au debut de la fenetre, donc un compteur
|
||||||
|
// immobile (contacteur ouvert toute la soiree) a bien une valeur.
|
||||||
|
const IDS_IMPORT = [...COMPTEURS_IMPORT, COMPTEUR_EDF];
|
||||||
|
let minuitCache = { jour: null, valeurs: null };
|
||||||
|
async function valeursAMinuit() {
|
||||||
|
const minuit = minuitLocal(new Date());
|
||||||
|
const jour = minuit.toISOString();
|
||||||
|
if (minuitCache.jour === jour) return minuitCache.valeurs;
|
||||||
|
const debut = new Date(minuit.getTime() - 6 * 3600e3).toISOString();
|
||||||
|
const r = await fetch(
|
||||||
|
`${HA_URL}/api/history/period/${debut}?end_time=${encodeURIComponent(jour)}` +
|
||||||
|
`&filter_entity_id=${IDS_IMPORT.join(',')}&minimal_response&no_attributes`,
|
||||||
|
{ headers: { Authorization: `Bearer ${HA_TOKEN}` }, signal: AbortSignal.timeout(15000) },
|
||||||
|
);
|
||||||
|
if (!r.ok) throw new Error(`historique HA ${r.status}`);
|
||||||
|
const series = await r.json();
|
||||||
|
const valeurs = {};
|
||||||
|
for (const serie of series) {
|
||||||
|
const id = serie[0]?.entity_id;
|
||||||
|
const nums = serie.map((e) => Number(e.state)).filter(Number.isFinite);
|
||||||
|
if (id) valeurs[id] = nums.length ? nums[nums.length - 1] : null;
|
||||||
|
}
|
||||||
|
// On ne met en cache qu'une lecture complete : sinon on reessaie au cycle suivant.
|
||||||
|
if (COMPTEURS_IMPORT.every((id) => Number.isFinite(valeurs[id]))) minuitCache = { jour, valeurs };
|
||||||
|
return valeurs;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function publierImportJour(st) {
|
||||||
|
const minuit = await valeursAMinuit();
|
||||||
|
const courant = {};
|
||||||
|
for (const id of IDS_IMPORT) {
|
||||||
|
const v = Number(st.get(id)?.state);
|
||||||
|
courant[id] = Number.isFinite(v) ? v : null;
|
||||||
|
}
|
||||||
|
const ij = importJour(courant, minuit);
|
||||||
|
await publierCapteur('sensor.evcs_import_jour_kwh', ij.etat, {
|
||||||
|
friendly_name: 'EVCS — energie importee d\'EDF depuis minuit', icon: 'mdi:transmission-tower-import',
|
||||||
|
unit_of_measurement: typeof ij.etat === 'number' ? 'kWh' : undefined, ...ij.attributs,
|
||||||
|
calcule_a: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Bilan de la nuit pour l'agent vocal : les donnees du rapport du matin (meme
|
// Bilan de la nuit pour l'agent vocal : les donnees du rapport du matin (meme
|
||||||
// code, donc memes chiffres que le courriel). Toutes les 5 min suffit.
|
// code, donc memes chiffres que le courriel). Toutes les 5 min suffit.
|
||||||
let bilanNuitPublie = false; // publie des le premier cycle, puis toutes les 5 min
|
let bilanNuitPublie = false; // publie des le premier cycle, puis toutes les 5 min
|
||||||
|
|||||||
Reference in New Issue
Block a user