workspace: Jeep auto-identifiee, photo de tableau de bord, releves km, transfert Stoik
- concierge-live.js : variante (a), la JEEP identifiee par Uconnect recoit une ligne d'info sans question ; attente de 15 min avant de questionner une voiture inconnue ; releves km ; reponse sur une ligne ; simulation sans ecriture d'etat. - concierge-write.js (EVCS) : niveau et releve km, tests. - lecture-tableau.py : lecture d'une photo de tableau de bord par la Spark, confirmation avant envoi au concierge, % ignore si branchee depuis > 10 min. - stoik-transfert.py : rapports Stoik de la boite AIRTRIA vers la Gmail, une fois. - AGENTS.md et reference-concierge.md alignes. - .gitignore : deux scripts ajoutes, DREAMS de l'agent bilans exclu.
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// ⚠️ Trois constantes, pas une de plus. Ne JAMAIS les rendre dynamiques.
|
||||
// ⚠️ Quatre constantes, pas une de plus. Ne JAMAIS les rendre dynamiques.
|
||||
// La troisieme (niveau saisi) est une DECISION du proprietaire du 03/10/2026 :
|
||||
// le niveau annonce dans un message (« JEEP reste 70 % ») etait perdu, la regle
|
||||
// de fiche qui devait le faire ecrire n'etait appliquee qu'une fois sur trois,
|
||||
@@ -26,6 +26,10 @@ const path = require('path');
|
||||
const HELPER_ENTITY = 'input_text.airtria_vehicle_label';
|
||||
const SELECTOR_ENTITY = 'input_select.victron_evcs_vehicle';
|
||||
const LEVEL_ENTITY = 'input_number.victron_evcs_vehicle_level_pct';
|
||||
// La quatrieme (04/10/2026, decision du proprietaire) : la file des RELEVES
|
||||
// KILOMETRIQUES, que le service EVCS ingere (km par kWh, usure des batteries).
|
||||
// Tableau JSON des derniers releves [vehicule, km, niveau|null, epoch_s, source].
|
||||
const RELEVE_ENTITY = 'input_text.evcs_releve_km';
|
||||
// Liste EN DUR, volontairement PAS lue depuis HA : le sélecteur ne pilote que
|
||||
// les deux véhicules du foyer (départs VW 08:50 / JEEP 06:30). Si une option
|
||||
// est ajoutée côté HA un jour (ZERO ?), l'ajouter ici est une DÉCISION.
|
||||
@@ -135,7 +139,49 @@ async function setVehicleLevel(niveau, source) {
|
||||
return { entity: LEVEL_ENTITY, value: niveau };
|
||||
}
|
||||
|
||||
// Ajoute des releves a la file (garde les derniers dans 255 caracteres). Pure.
|
||||
function fileReleves(brut, releves) {
|
||||
let tab = [];
|
||||
try { const t = JSON.parse(brut || '[]'); if (Array.isArray(t)) tab = t; } catch { /* repart a vide */ }
|
||||
for (const r of releves) tab.push([r.vehicule, r.km, r.niveau ?? null, r.ts, r.source]);
|
||||
let out = tab.slice(-6);
|
||||
while (JSON.stringify(out).length > MAX_LEN && out.length > 1) out = out.slice(1);
|
||||
return JSON.stringify(out);
|
||||
}
|
||||
|
||||
// Releves kilometriques ANNONCES par le proprietaire. Vehicule du foyer, km
|
||||
// entier positif, niveau entier 0-100 ou absent : sinon REFUS, jamais de
|
||||
// correction. Source humaine. Relecture comme pour les autres ecritures.
|
||||
async function setReleveKm(releves, source) {
|
||||
if (!HUMAN_SOURCES.includes(source)) {
|
||||
throw new Error(`releve km : source '${source}' non humaine — écriture interdite`);
|
||||
}
|
||||
if (!Array.isArray(releves) || !releves.length) throw new Error('releve km : rien a ecrire');
|
||||
for (const r of releves) {
|
||||
if (!SELECTOR_LABELS.includes(r.vehicule)) throw new Error(`releve km : '${r.vehicule}' hors foyer`);
|
||||
if (!Number.isInteger(r.km) || r.km <= 0 || r.km > 2000000) throw new Error(`releve km : '${r.km}' invalide`);
|
||||
if (r.niveau !== null && !(Number.isInteger(r.niveau) && r.niveau >= 0 && r.niveau <= 100)) {
|
||||
throw new Error(`releve km : niveau '${r.niveau}' invalide`);
|
||||
}
|
||||
if (!Number.isInteger(r.ts)) throw new Error('releve km : horodatage invalide');
|
||||
}
|
||||
const token = haToken();
|
||||
const headers = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };
|
||||
const cur = await fetch(`${HA_BASE}/api/states/${RELEVE_ENTITY}`, { headers });
|
||||
if (!cur.ok) throw new Error(`lecture file HTTP ${cur.status}`);
|
||||
const payload = fileReleves((await cur.json()).state, releves.map(r => ({ ...r, source })));
|
||||
const res = await fetch(`${HA_BASE}/api/services/input_text/set_value`, {
|
||||
method: 'POST', headers,
|
||||
body: JSON.stringify({ entity_id: RELEVE_ENTITY, value: payload }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`set_value releve HTTP ${res.status}`);
|
||||
const back = await fetch(`${HA_BASE}/api/states/${RELEVE_ENTITY}`, { headers });
|
||||
if (!back.ok) throw new Error(`relecture HTTP ${back.status}`);
|
||||
if ((await back.json()).state !== payload) throw new Error('relecture != écrit — releve refusé en silence par HA');
|
||||
return { entity: RELEVE_ENTITY, payload };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
publishLabel, setVehicleSelector, setVehicleLevel,
|
||||
HELPER_ENTITY, SELECTOR_ENTITY, LEVEL_ENTITY, SELECTOR_LABELS, MAX_LEN,
|
||||
publishLabel, setVehicleSelector, setVehicleLevel, setReleveKm, fileReleves,
|
||||
HELPER_ENTITY, SELECTOR_ENTITY, LEVEL_ENTITY, RELEVE_ENTITY, SELECTOR_LABELS, MAX_LEN,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user