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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user