- Fiche agent maison (AGENTS.md) : prevision de nuit relayee par script, import
EDF, regle JEEP midi, saisie vehicule/niveau/depart, concierge qui relaie
« ✅ Noté » ou « Rien n'est noté », groupe A deplace (28 649 -> 25 831 car.).
- Nouvelles references : reference-concierge.md, reference-workspace.md.
- Scripts : rapport-solaire.js, prevision-nuit.js, airtria-ha-vehicule.js,
concierge-live.js (answer ne sort plus jamais vide, mode CONCIERGE_SIMULATION).
- Courrier Orange / AIRTRIA / Gmail : audits, regles, menage, recents.
- Bancs EVCS tests-pv (G5 accepte « anomal »).
- .gitignore : liste blanche etendue aux nouveaux scripts et bancs.
167 lines
9.8 KiB
JavaScript
167 lines
9.8 KiB
JavaScript
#!/usr/bin/env node
|
|
// rapport-solaire.js — RAPPORT SOLAIRE type, imprimé tout formaté pour l'agent.
|
|
// -----------------------------------------------------------------------------
|
|
// Composition validée par Thierry le 26/09/2026, calquée sur la page VRM :
|
|
// 1. en-tête (date, heure, météo) 2. production (réel du jour, détail par chargeur,
|
|
// PV maintenant, pic par chargeur) 6. consommation du jour (EVCS) 4. réseau EDF
|
|
// 5. batterie + conclusion.
|
|
// Hors rapport pour l'instant : prévision (Forecast.Solar sous-estime de 63 % sur 7 j,
|
|
// EVCS cherche un accès VRM), véhicule. On les ajoutera quand EVCS les fournit.
|
|
//
|
|
// Doctrine : le script LIT et MET EN FORME, il ne calcule aucun verdict. L'agent
|
|
// relaie la sortie telle quelle. Une entité absente s'affiche « indisponible »,
|
|
// jamais une valeur inventée ni une valeur d'hier.
|
|
//
|
|
// Usage : node rapport-solaire.js → version complète (mail / écrit)
|
|
// node rapport-solaire.js --vocal → version courte (6 phrases, à lire)
|
|
// -----------------------------------------------------------------------------
|
|
const fs = require('node:fs');
|
|
|
|
const HA = process.env.HA_BASE || 'http://10.10.10.17:8123';
|
|
const TOKEN = fs.readFileSync(`${__dirname}/.ha-token`, 'utf8').trim();
|
|
const VOCAL = process.argv.includes('--vocal');
|
|
|
|
const E = {
|
|
prod: 'sensor.evcs_production_jour_kwh',
|
|
conso: 'sensor.evcs_consommation_jour_kwh',
|
|
diag: 'sensor.evcs_pv_diagnostic',
|
|
reseau: 'sensor.evcs_reseau',
|
|
importJour: 'sensor.evcs_import_jour_kwh',
|
|
gridOn: 'input_datetime.victron_grid_last_on_at',
|
|
gridOff: 'input_datetime.victron_grid_last_off_at',
|
|
soc: 'sensor.victron_battery_soc',
|
|
battP: 'sensor.victron_battery_power',
|
|
battState: 'sensor.gx_device_dc_battery_state',
|
|
battV: 'sensor.victron_battery_voltage',
|
|
battT: 'sensor.victron_battery_temperature',
|
|
cibleNuit: 'input_number.victron_evcs_night_target_soc',
|
|
nuit: 'sensor.evcs_bilan_nuit',
|
|
cellules: 'sensor.evcs_cellules',
|
|
meteo: 'weather.forecast_maison',
|
|
pluie: 'sensor.meteo_france_forecast_for_city_pleumeur_bodou_bretagne_22_fr_pleumeur_bodou_rain_chance',
|
|
vigilance: 'sensor.meteo_france_alert_for_department_22_22_weather_alert',
|
|
picMppt: 'sensor.mppt_450_200_hq2449yr9pt_max_power_today',
|
|
picAirtria: 'sensor.airtria_mppt1_max_power_today',
|
|
picV1: 'sensor.v1_max_power_today',
|
|
};
|
|
const NOMS = { mppt_450_200: 'MPPT 450/200', airtria_mppt1: 'AIRTRIA MPPT1', v1: 'V1' };
|
|
const METEO_FR = { sunny: 'ensoleillé', clearnight: 'ciel dégagé', partlycloudy: 'partiellement nuageux',
|
|
cloudy: 'couvert', rainy: 'pluie', pouring: 'fortes pluies', fog: 'brouillard', windy: 'venteux',
|
|
lightning: 'orage', snowy: 'neige', hail: 'grêle' };
|
|
const ETAT_BATT = { charging: 'en charge', discharging: 'en décharge', idle: 'au repos' };
|
|
const ETAT_RESEAU = { non_raccorde: 'non raccordé', raccorde: 'raccordé', etablissement: 'en cours de raccordement',
|
|
mesure_muette: 'mesure muette', incoherent: 'incohérent', indetermine: 'indéterminé' };
|
|
|
|
async function get(id) {
|
|
try {
|
|
const r = await fetch(`${HA}/api/states/${id}`, { headers: { Authorization: `Bearer ${TOKEN}` } });
|
|
if (!r.ok) return null;
|
|
const j = await r.json();
|
|
if (['unavailable', 'unknown', ''].includes(String(j.state))) return null;
|
|
return j;
|
|
} catch { return null; }
|
|
}
|
|
const n = (v, d = 1) => (v == null || isNaN(Number(v))) ? 'indisponible'
|
|
: Number(v).toLocaleString('fr-FR', { minimumFractionDigits: 0, maximumFractionDigits: d });
|
|
const kwh = (v, d = 1) => v == null ? 'indisponible' : `${n(v, d)} kWh`;
|
|
const w = v => v == null ? 'indisponible' : `${n(Math.round(v), 0)} W`;
|
|
const hhmm = iso => { if (!iso) return 'indisponible';
|
|
const d = new Date(iso.replace(' ', 'T')); if (isNaN(d)) return String(iso);
|
|
return d.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Paris' }); };
|
|
|
|
(async () => {
|
|
const s = {};
|
|
await Promise.all(Object.entries(E).map(async ([k, id]) => { s[k] = await get(id); }));
|
|
const st = k => s[k] ? s[k].state : null;
|
|
const at = (k, a) => s[k] ? s[k].attributes[a] : undefined;
|
|
|
|
const now = new Date();
|
|
const dateFr = now.toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric', timeZone: 'Europe/Paris' });
|
|
const heure = now.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Paris' }).replace(':', 'h');
|
|
|
|
// 1. météo
|
|
const cond = METEO_FR[st('meteo')] || st('meteo') || 'indisponible';
|
|
const temp = at('meteo', 'temperature');
|
|
const nuages = at('meteo', 'cloud_coverage');
|
|
|
|
// 2. production
|
|
const prod = st('prod'); const statut = at('prod', 'statut'); const muets = at('prod', 'chargeurs_muets') || [];
|
|
let detail = at('prod', 'detail_kwh'); if (typeof detail === 'string') { try { detail = JSON.parse(detail); } catch { detail = null; } }
|
|
const prodTxt = prod == null ? 'indisponible'
|
|
: statut === 'partiel' ? `au moins ${kwh(prod)} (partiel : ${muets.join(', ') || 'chargeur muet'})`
|
|
: `${kwh(prod)} (${statut || 'statut inconnu'})`;
|
|
const detailTxt = detail ? Object.entries(detail).map(([k, v]) => `${NOMS[k] || k} ${n(v, 1)}`).join(' · ') : 'indisponible';
|
|
const pvW = at('diag', 'pv_w'); const diagEtat = st('diag'); const diagExpl = at('diag', 'explication');
|
|
const pics = [['MPPT 450/200', st('picMppt')], ['AIRTRIA MPPT1', st('picAirtria')], ['V1', st('picV1')]]
|
|
.filter(([, v]) => v != null).map(([k, v]) => `${k} ${w(v)}`).join(' · ') || 'indisponible';
|
|
|
|
// diagnostic en une phrase courte pour l'oral (les phrases longues sont coupées par la synthèse vocale du téléphone)
|
|
const DIAG_COURT = { bridage_normal: 'bridage normal, du solaire reste disponible pour une charge de plus' };
|
|
const diagCourt = diagEtat == null ? 'indisponible'
|
|
: DIAG_COURT[diagEtat] || (diagExpl ? String(diagExpl).split(/(?<=\.)\s/)[0].replace(/\.$/, '') : String(diagEtat).replace(/_/g, ' '));
|
|
|
|
// 6. consommation (capteur EVCS, maison + borne, depuis minuit)
|
|
const conso = st('conso'); const consoExpl = at('conso', 'explication');
|
|
let cdet = at('conso', 'detail_kwh'); if (typeof cdet === 'string') { try { cdet = JSON.parse(cdet); } catch { cdet = null; } }
|
|
const consoTxt = conso == null ? `indisponible${consoExpl ? ` — ${consoExpl}` : ''}`
|
|
: at('conso', 'statut') === 'partiel' ? `au moins ${kwh(conso)} (partiel)`
|
|
: `${kwh(conso)}${cdet ? ` (réseau ${n(cdet.depuis_reseau, 1)}, onduleur ${n(cdet.depuis_onduleur, 1)})` : ''}`;
|
|
const consoVocal = conso == null ? 'Consommation du jour indisponible.'
|
|
: `Consommation du jour : ${at('conso', 'statut') === 'partiel' ? 'au moins ' : ''}${kwh(conso)}${cdet ? `, dont ${kwh(cdet.depuis_reseau)} depuis le réseau` : ''}.`;
|
|
|
|
// 4. réseau
|
|
const rEtat = ETAT_RESEAU[st('reseau')] || st('reseau') || 'indisponible';
|
|
const rDepuis = at('reseau', 'depuis_heure'); const rExpl = at('reseau', 'explication');
|
|
const imp = st('importJour');
|
|
|
|
// 5. batterie
|
|
const soc = st('soc'); const bp = st('battP'); const bEtat = ETAT_BATT[st('battState')] || st('battState') || '';
|
|
const bTxt = soc == null ? 'indisponible' : `${n(soc, 0)} %${bEtat ? `, ${bEtat}` : ''}${bp != null ? ` à ${w(Math.abs(bp))}` : ''}`;
|
|
const nuitDep = at('nuit', 'batterie_maison_depart_pct'); const nuitBas = at('nuit', 'batterie_maison_plus_bas_pct');
|
|
const nuitH = at('nuit', 'batterie_maison_heure_plus_bas');
|
|
const cellOk = at('cellules', 'statut') === 'frais';
|
|
const cellTxt = !s.cellules ? 'indisponible' : cellOk
|
|
? `module ${at('cellules', 'repere_le_plus_sensible')} (${at('cellules', 'module_le_plus_sensible')}), écart ${n(st('cellules'), 0)} mV, relevé frais`
|
|
: `relevé ${at('cellules', 'statut') || 'périmé'} : ne pas annoncer de module`;
|
|
|
|
if (VOCAL) {
|
|
const phrases = [
|
|
`Rapport solaire de ${heure}, temps ${cond}${temp != null ? `, ${n(temp, 0)} degrés` : ''}.`,
|
|
`Production du jour : ${prodTxt}${pvW != null ? `, ${w(pvW)} en ce moment` : ''}.`,
|
|
consoVocal,
|
|
`Réseau EDF ${rEtat}${rDepuis ? ` depuis ${rDepuis}` : ''}, ${imp != null ? `${kwh(imp)} importés aujourd'hui` : 'import indisponible'}.`,
|
|
`Batterie à ${bTxt}${s.battT ? `, ${n(st('battT'), 0)} degrés` : ''}.`,
|
|
`Diagnostic : ${diagCourt}.`,
|
|
];
|
|
console.log(phrases.join('\n'));
|
|
return;
|
|
}
|
|
|
|
const L = [];
|
|
L.push(`RAPPORT SOLAIRE — ${dateFr}, ${heure}`);
|
|
L.push(`Météo : ${cond}${temp != null ? `, ${n(temp, 0)} °C` : ''}${nuages != null ? `, nuages ${n(nuages, 0)} %` : ''}${st('pluie') != null ? `, pluie ${n(st('pluie'), 0)} %` : ''}${st('vigilance') ? `, vigilance ${st('vigilance')}` : ''}`);
|
|
L.push('');
|
|
L.push('PRODUCTION');
|
|
L.push(`Réalisé du jour : ${prodTxt}`);
|
|
L.push(`Par chargeur (kWh) : ${detailTxt}`);
|
|
L.push(`PV en ce moment : ${w(pvW)}${diagEtat ? ` — état ${diagEtat}` : ''}`);
|
|
L.push(`Pic du jour : ${pics}`);
|
|
L.push('');
|
|
L.push('CONSOMMATION');
|
|
L.push(`Depuis minuit (maison et borne) : ${consoTxt}`);
|
|
L.push('');
|
|
L.push('RÉSEAU EDF');
|
|
L.push(`État : ${rEtat}${rDepuis ? ` depuis ${rDepuis}` : ''}${rExpl ? ` — ${rExpl}` : ''}`);
|
|
L.push(`Importé aujourd'hui : ${kwh(imp)} — injecté : 0 kWh (installation sans injection)`);
|
|
L.push(`Contacteur : dernière fermeture ${hhmm(st('gridOn'))}, dernière ouverture ${hhmm(st('gridOff'))}`);
|
|
L.push('');
|
|
L.push('BATTERIE MAISON');
|
|
L.push(`SOC : ${bTxt}${s.battV ? ` — ${n(st('battV'), 1)} V` : ''}${s.battT ? `, ${n(st('battT'), 0)} °C` : ''}`);
|
|
L.push(`Nuit passée : ${nuitDep != null ? `départ ${n(nuitDep, 0)} %, plus bas ${n(nuitBas, 0)} %${nuitH ? ` à ${nuitH}` : ''}` : 'indisponible'} — objectif SOC nuit : ${st('cibleNuit') != null ? `${n(st('cibleNuit'), 0)} %` : 'indisponible'}`);
|
|
L.push(`Cellule la plus sensible : ${cellTxt}`);
|
|
L.push('');
|
|
L.push('CONCLUSION');
|
|
L.push(diagExpl || 'Diagnostic indisponible.');
|
|
console.log(L.join('\n'));
|
|
})();
|