#!/usr/bin/env node import { cleCourte, lireAcquittements, pilesAAlerter } from '../../tools/piles.mjs'; const T = Date.parse('2026-10-10T00:30:00+02:00'); const e = (entity_id, state, attrs = {}, last = '2026-10-09T22:00:00+02:00') => ({ entity_id, state, attributes: attrs, last_changed: last }); // Extrait reel des etats HA du 10/10 00:30. const etats = [ e('sensor.victron_battery_soc', '25', { device_class: 'battery', unit_of_measurement: '%' }), e('sensor.iphone_16_de_tt_battery_level', '15', { device_class: 'battery', unit_of_measurement: '%' }), e('sensor.jeep_renegade_phev_hvbattery_charge', '10', { device_class: 'battery', unit_of_measurement: '%' }), e('sensor.sdb_temp_hygro_batterie', '0.0', { device_class: 'battery', unit_of_measurement: '%', friendly_name: 'SDB TEMP HYGRO Batterie' }), e('sensor.kt_chambre_batterie', '12.0', { device_class: 'battery', unit_of_measurement: '%', friendly_name: 'SYLVIE CHAMBRE Batterie' }), e('sensor.salle_de_bain_bruz_batterie', '27.0', { device_class: 'battery', unit_of_measurement: '%', friendly_name: 'GRENIER Batterie' }), e('sensor.tt_chambre_batterie', 'unavailable', { device_class: 'battery', unit_of_measurement: '%', friendly_name: 'TT CHAMBRE Batterie' }, '2026-10-04T14:52:01+02:00'), e('sensor.garage_t_etat_batterie', 'middle', { device_class: 'enum', friendly_name: 'Garage T État batterie' }), e('sensor.cuisine_etat_batterie', 'low', { device_class: 'enum', friendly_name: 'TRETS CUISINE État batterie' }), e('sensor.chambre_danielle_etat_batterie', 'unavailable', { device_class: 'enum', friendly_name: 'Chambre danielle État batterie' }, '2026-10-09T23:00:00+02:00'), ]; let f = 0; const ok = (l, c, d = '') => { if (c) console.log(`✅ ${l}`); else { f++; console.log(`❌ ${l} ${d}`); } }; const r = pilesAAlerter(etats, T); const ids = r.alertes.map((a) => a.entite); ok('exclus-victron-telephone-voiture', !ids.some((i) => /victron|iphone|jeep/.test(i)), ids.join(',')); ok('pourcentage-faible', ids.includes('sensor.sdb_temp_hygro_batterie') && ids.includes('sensor.kt_chambre_batterie') && !ids.includes('sensor.salle_de_bain_bruz_batterie')); ok('enum-low', ids.includes('sensor.cuisine_etat_batterie') && !ids.includes('sensor.garage_t_etat_batterie')); ok('muet-plus-de-24h', r.alertes.find((a) => a.entite === 'sensor.tt_chambre_batterie')?.statut === 'muet'); ok('muet-recent-pas-encore', !ids.includes('sensor.chambre_danielle_etat_batterie')); ok('ignorees', !pilesAAlerter(etats, T, ['sensor.kt_chambre_batterie']).alertes.some((a) => a.entite === 'sensor.kt_chambre_batterie')); ok('texte', /SYLVIE CHAMBRE Batterie : pile a 12 %/.test(r.texte) && /TT CHAMBRE Batterie ne publie plus depuis 5 j/.test(r.texte), r.texte); // 13/10 : hysteresis. GRENIER 13 -> 19 % -> 6 % : une seule alerte, qui tient tant que < 40 %. const grenier = (n) => [{ entity_id: 'sensor.salle_de_bain_bruz_batterie', state: String(n), last_changed: new Date(T).toISOString(), attributes: { device_class: 'battery', unit_of_measurement: '%', friendly_name: 'GRENIER Batterie' } }]; ok('hysteresis-tient-a-25', pilesAAlerter(grenier(25), T, [], { precedentes: ['sensor.salle_de_bain_bruz_batterie'] }).alertes.length === 1); ok('hysteresis-pas-de-nouvelle-a-25', pilesAAlerter(grenier(25), T, [], { precedentes: [] }).alertes.length === 0); ok('hysteresis-retombe-a-40', pilesAAlerter(grenier(40), T, [], { precedentes: ['sensor.salle_de_bain_bruz_batterie'] }).alertes.length === 0); // Acquittement : « vu, je change dans 3 jours » -> plus de rappel jusqu'a la date. const kG = cleCourte('sensor.salle_de_bain_bruz_batterie'); const acq = lireAcquittements(`2026-10-16:${kG}|abcdef, mauvais format, 2026-10-12:zz`); const ra = pilesAAlerter(grenier(6), Date.parse('2026-10-14T09:00:00+02:00'), [], { acquittements: acq }); ok('acquittee-pas-de-rappel', ra.alertes.length === 1 && ra.a_rappeler.length === 0 && ra.acquittees[0].jusqu_au === '2026-10-16' && /jusqu'au 16\/10/.test(ra.texte), JSON.stringify(ra)); const rb = pilesAAlerter(grenier(6), Date.parse('2026-10-17T08:00:00+02:00'), [], { acquittements: acq }); ok('acquittement-expire-rappel', rb.a_rappeler.length === 1 && rb.a_rappeler[0].cle === kG && /^[0-9a-f]{6}$/.test(kG)); ok('acquittement-format-tolerant', Object.keys(acq).length === 2 && acq[kG].jusquAu === '2026-10-16'); // 9 alertes groupees sur une date : bien sous les 255 caracteres du helper. ok('format-groupe-tient-dans-255', `2026-10-20:${Array.from({ length: 9 }, (_, i) => cleCourte('sensor.x' + i)).join('|')}`.length < 80); const gerard = (v) => [{ entity_id: 'sensor.chambre_gerard_etat_batterie', state: v, last_changed: new Date(T).toISOString(), attributes: { device_class: 'enum', friendly_name: 'Chambre Gerard Etat batterie' } }]; ok('enum-low-middle-tient', pilesAAlerter(gerard('middle'), T, [], { precedentes: ['sensor.chambre_gerard_etat_batterie'] }).alertes.length === 1 && pilesAAlerter(gerard('middle'), T, []).alertes.length === 0 && pilesAAlerter(gerard('high'), T, [], { precedentes: ['sensor.chambre_gerard_etat_batterie'] }).alertes.length === 0); console.log(f ? `\n${f} ECHEC(S)` : '\nPILES OK'); process.exit(f ? 1 : 0);