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:
2026-10-04 15:50:25 +02:00
parent b6dccc0481
commit 4cd3615172
11 changed files with 564 additions and 40 deletions

View File

@@ -1,7 +1,8 @@
// concierge-write.test.js — Test de PORTÉE D'ÉCRITURE (garde-fou du canal HA).
// Doit ÉCHOUER si une écriture peut sortir des TROIS constantes autorisées :
// Doit ÉCHOUER si une écriture peut sortir des QUATRE constantes autorisées :
// airtria_vehicle_label (étiquette), victron_evcs_vehicle (sélecteur, ajouté
// 19/09/2026) et victron_evcs_vehicle_level_pct (niveau annoncé, 03/10/2026). À faire tourner à CHAQUE modification (cf. EVCS advisor).
// 19/09/2026) et victron_evcs_vehicle_level_pct (niveau annoncé, 03/10/2026) et evcs_releve_km
// (relevés kilométriques, 04/10/2026). À faire tourner à CHAQUE modification (cf. EVCS advisor).
// Statique + require + fetch STUBBÉ : aucun réseau, aucun token, aucune écriture.
'use strict';
@@ -25,23 +26,25 @@ check('constantes = airtria_vehicle_label + victron_evcs_vehicle + niveau', () =
assert.strictEqual(mod.HELPER_ENTITY, 'input_text.airtria_vehicle_label');
assert.strictEqual(mod.SELECTOR_ENTITY, 'input_select.victron_evcs_vehicle');
assert.strictEqual(mod.LEVEL_ENTITY, 'input_number.victron_evcs_vehicle_level_pct');
assert.strictEqual(mod.RELEVE_ENTITY, 'input_text.evcs_releve_km');
assert.deepStrictEqual(mod.SELECTOR_LABELS, ['VW', 'JEEP', 'ZERO']);
});
// 2. entity_id n'est JAMAIS accolé à autre chose que l'une des DEUX constantes
// (pas de littéral d'entité, pas d'interpolation, pas de concaténation).
check('entity_id uniquement = HELPER_ENTITY, SELECTOR_ENTITY ou LEVEL_ENTITY', () => {
check('entity_id uniquement = HELPER_ENTITY, SELECTOR_ENTITY, LEVEL_ENTITY ou RELEVE_ENTITY', () => {
const occ = [...src.matchAll(/entity_id\s*:\s*([^,}\n]+)/g)].map(m => m[1].trim());
assert(occ.length >= 3, `entity_id attendus >= 3, trouvés ${occ.length}`);
for (const v of occ) assert(['HELPER_ENTITY', 'SELECTOR_ENTITY', 'LEVEL_ENTITY'].includes(v),
for (const v of occ) assert(['HELPER_ENTITY', 'SELECTOR_ENTITY', 'LEVEL_ENTITY', 'RELEVE_ENTITY'].includes(v),
`entity_id dynamique ou littéral interdit : ${v}`);
});
// 3. Aucune entité HA en dur hors les DEUX déclarations de constantes.
check('aucune entité HA littérale hors les trois constantes', () => {
check('aucune entité HA littérale hors les quatre constantes', () => {
const decls = ["'input_text.airtria_vehicle_label'",
"'input_select.victron_evcs_vehicle'",
"'input_number.victron_evcs_vehicle_level_pct'"];
"'input_number.victron_evcs_vehicle_level_pct'",
"'input_text.evcs_releve_km'"];
const lits = [...src.matchAll(/['"`]([a-z_]+\.[a-z0-9_]+)['"`]/g)].map(m => m[0]);
for (const l of lits) assert(decls.includes(l),
`littéral d'entité interdit : ${l}`);
@@ -52,7 +55,7 @@ check('services = input_text/set_value + input_select/select_option + input_numb
const services = [...src.matchAll(/\/api\/services\/([a-z_]+\/[a-z_]+)/g)]
.map(m => m[1]);
assert.deepStrictEqual(services.sort(),
['input_number/set_value', 'input_select/select_option', 'input_text/set_value'],
['input_number/set_value', 'input_select/select_option', 'input_text/set_value', 'input_text/set_value'],
`services inattendus : ${services.join(', ') || '(aucun)'}`);
});
@@ -76,6 +79,43 @@ global.fetch = async (url, opts) => {
};
process.env.HA_TOKEN_FILE = '/dev/null';
// R1-R4. Releves km : refus SANS aucun appel reseau (04/10/2026).
for (const [nom, args] of [
['releve refuse une source non humaine', [[{ vehicule: 'JEEP', km: 1000, niveau: 50, ts: 1791066924 }], 'regle']],
['releve refuse un vehicule hors foyer', [[{ vehicule: 'MEGANE', km: 1000, niveau: 50, ts: 1791066924 }], 'whatsapp']],
['releve refuse un niveau de 150', [[{ vehicule: 'JEEP', km: 1000, niveau: 150, ts: 1791066924 }], 'whatsapp']],
['releve refuse un km non entier', [[{ vehicule: 'JEEP', km: 1000.5, niveau: null, ts: 1791066924 }], 'whatsapp']],
]) {
check(nom, async () => {
const avant = calls.length;
await assert.rejects(() => mod.setReleveKm(...args));
assert.strictEqual(calls.length, avant, 'aucun appel reseau attendu');
});
}
check('file des releves : 255 caracteres max, dernier garde', () => {
let b = '';
for (let i = 0; i < 10; i++) b = mod.fileReleves(b, [{ vehicule: 'JEEP', km: 112694 + i, niveau: 54, ts: 1791066924 + i, source: 'whatsapp' }]);
assert(b.length <= 255, `longueur ${b.length}`);
assert.strictEqual(JSON.parse(b).at(-1)[1], 112703);
});
check('releve ecrit la file puis la relit', async () => {
const avant = calls.length;
const stub = global.fetch;
let ecrit = null;
global.fetch = async (url, opts) => {
calls.push({ url: String(url), body: opts && opts.body });
if (opts && opts.method === 'POST') { ecrit = JSON.parse(opts.body).value; return { ok: true, json: async () => ({}) }; }
return { ok: true, json: async () => ({ state: ecrit ?? '[]' }) };
};
try {
await mod.setReleveKm([{ vehicule: 'ZERO', km: 16469, niveau: 78, ts: 1791066924 }], 'whatsapp');
} finally { global.fetch = stub; }
const posts = calls.slice(avant).filter(c => c.body);
assert.strictEqual(posts.length, 1);
assert.strictEqual(JSON.parse(posts[0].body).entity_id, 'input_text.evcs_releve_km');
assert.deepStrictEqual(JSON.parse(ecrit), [['ZERO', 16469, 78, 1791066924, 'whatsapp']]);
});
// 6. Refus d'une source non humaine, sans AUCUN appel réseau.
check('sélecteur refuse une source non humaine', async () => {
calls.length = 0;