SDB : capteur muet et garde batterie (workflow sous controle du depot) ; alertes de piles des capteurs
This commit is contained in:
26
HANDOFF.md
26
HANDOFF.md
@@ -2277,5 +2277,29 @@ batterie a 32 A et JEEP (branchee 13:01, 52 %) qui ne demarre pas. SOC 47 % a 12
|
||||
- ⚠️ JEEP part samedi 06:30 : mode de nuit « complete » conseille ce soir (aucune regle ne charge une voiture a
|
||||
depart connu en « sans EDF » si EDF ne se ferme pas pour la batterie — cas du planificateur).
|
||||
|
||||
### 10/10 00:15 — SDB : deshumidificateur bloque allume par un capteur muet ; alertes de piles
|
||||
- Constat du proprietaire (captures EVCS SDB Premium) : capteur `sensor.sdb_temp_hygro_*` `unavailable` du 04/10 14:52
|
||||
(redemarrage HA) au 10/10 00:20 (piles changees par lui). Le workflow « SDB Climate & Surplus Manager V2.2 »
|
||||
(`2NhrfxuGm4pTUNs9`) maintenait le deshumidificateur « jusqu'a H <= 41 % » : sans mesure, jamais -> deshumidificateur
|
||||
(~150 W) depuis 20:00 et prise SDB (~500 W) depuis 22:39, la nuit, SOC 26 %, EDF ouvert. Aucun garde batterie, aucune
|
||||
alerte.
|
||||
- **00:23 (accord du proprietaire)** : deshumidificateur et prise SDB coupes, latches remis a off (sinon rallumes au
|
||||
cycle suivant). Etat `idle`.
|
||||
- **Workflow sous controle du depot** (`workflows/sdb-climate-surplus-v2.2.json`, `src/sdb/`, entree `sdb` du node-map,
|
||||
tests `node tests/sdb/run.mjs` 12/12, 5 echecs sur l'ancien code) : **capteur muet** (illisible ou derniere publication
|
||||
> 3 h, `sdb_humidity_age_min` par `last_reported`) -> rien n'est tenu au-dela du temps mini, pas de demarrage,
|
||||
notification « Capteur SDB muet » ; **energie** : le deshumidificateur (charge reportable) ne tourne sur la batterie
|
||||
que si SOC >= `evcs_balance_required_soc_prudent` (pas de seuil fixe), sinon EDF ferme ou vrai surplus ; bilan
|
||||
illisible -> comportement anterieur. Signature `muet=`, `nrj=`. Deploye 00:26, executions en succes ; 00:27 : capteur
|
||||
vivant (24,3 °C, 58 %), `nrj=0` (batterie reservee). Sauvegarde du code d'origine `sdb_compute.bak-20261010.js`
|
||||
(scratchpad) et la version d'origine du JSON est le premier commit du fichier.
|
||||
- **Piles** (`tools/piles.mjs`, tests `tests/piles/run.mjs` 7/7) : `sensor.evcs_piles` (toutes les 15 min) = alertes
|
||||
pile <= 20 % ou etat `low`, ou capteur de pile indisponible > 24 h ; exclus Victron, GX, parc, Jeep, telephones ;
|
||||
`input_text.evcs_piles_ignorees` (paquet `evcs_piles.yaml`) pour faire taire un appareil. Premier releve : **9
|
||||
alertes** (SDB a 0 % juste apres le changement — a revoir ; SYLVIE CHAMBRE 12 % ; sept capteurs muets depuis le 04/10,
|
||||
dont des chambres et « TRETS » : a confirmer par le proprietaire, peut-etre une autre maison -> liste d'ignores).
|
||||
SPARK previent par WhatsApp.
|
||||
- ⚠️ `switch.deshumide_sdb_socket_1` `unavailable` depuis 00:25 (prise debranchee ?).
|
||||
|
||||
---
|
||||
*Document mis a jour le : 09/10/2026 (~14:15) — conseiller relecteur de la journee (revue calculee + pistes) ; nuit : voiture qui profite d'EDF ferme pour la batterie, nuit qui prepare le soir les jours annonces faibles ; 08/10/2026 (~00:15) — jour faible : charge maximale en HC de midi (bilan 100 %, ACM plafond + solaire) ; 07/10/2026 (~22:45) — reprise de 16 h verifiee, EDF en HP a 18:03, bilan : reprise du soir et prevision recalee sur la matinee ; bilan HC de midi, reprise du soir apres coupure en pleine chauffe, mesure ECS sur 10 min ; recharge reseau HC : courant calcule (12 kVA) et consigne non tenue ; HC de midi : AUTO ne chauffe pas (BOOST), alerte v2, Jeep (contact, relecture, zone), recharge batterie bridee a 30 A par l'ACM ; jour faible = EDF les deux fenetres HC pour le chauffe-eau, alerte BOOST, Victron Mobile refait ; 1re decision jour faible en reel, mesure ECS reparee, douches 06:00/20:00, Grafana chauffe-eau, tableaux ECS ; chauffe-eau en HC de nuit les jours faibles (decision du bilan, workflow ECS sous controle du depot, VRM, V2.3 desactive) ; garde batterie reseau : voiture bridee a 6 A au lieu d'un arret ; garde-fou concierge 2 h ; allers-retours brefs de la borne retrouves dans l'historique HA (echange VW -> JEEP de 20 s), production 14,5 kWh contre 22 prevus, defaut garde batterie en mode reseau a trancher ; voiture a midi : la batterie n'est plus comptee comme du solaire (EDF automatique les jours de depart si le surplus ne couvre pas) ; nuit 05->06 : JEEP chargee sur EDF en complete (6,32 kWh), budget 0 du a la prevision, Windy/ECMWF contre VRM, a comparer ce soir ; concierge aligne sur la preuve fin de trajet ; a verifier : nuit 05->06 et mardi faible, comparaison VRM/Forecast.Solar ; JEEP identifiee aussi par sa fin de trajet, prevision de la nuit 05->06 ; identification JEEP reussie en reel, correction humaine qui prime (refus supprime), session du 04/10 coupee ; EVCS Master en quatre onglets ; Jeep lue par Uconnect et identifiee seule, capacite JEEP 10 kWh, concierge variante (a) ; releve par photo (SPARK), hass-uconnect en attente ; ecart semaine/week-end mesure (pas de profil week-end la nuit) ; nuit du budget : budget borne au plancher prudent, sans EDF = voiture bloquee meme EDF ferme (batterie seule) ; Energie : sauts corriges ; tableau Energie complete (Sud/Ouest, batterie par integration, borne ; aucun redemarrage HA) ; kilometrage des vehicules (km/kWh, capacite, releves de reference, agent cable 15/15) ; a verifier le 04/10 au matin : nuit du budget JEEP ; concierge : vehicule non confirme demande, heure de saisie ; Grafana vehicules en tableau ; Grafana vehicules : ZERO et Invite ajoutes, session 16:46 reetiquetee ZERO ; prevision SOC 10 h ~35 % ; concierge fiable : niveau note par le script, une ligne, dedoublonnage, simulation par drapeau ; agent : message incompris = question, bourse sur demande explicite (banc final 6/6) ; a verifier : nuit du samedi 03 au dimanche 04/10 (budget < besoin JEEP), cron orange-cloture lundi 05/10 apres 17:40, comparaison VRM / Forecast.Solar lundi 05/10*
|
||||
*Document mis a jour le : 10/10/2026 (~00:40) — conseiller relecteur de la journee (revue calculee + pistes) ; nuit : voiture qui profite d'EDF ferme pour la batterie, nuit qui prepare le soir les jours annonces faibles ; 08/10/2026 (~00:15) — jour faible : charge maximale en HC de midi (bilan 100 %, ACM plafond + solaire) ; 07/10/2026 (~22:45) — reprise de 16 h verifiee, EDF en HP a 18:03, bilan : reprise du soir et prevision recalee sur la matinee ; bilan HC de midi, reprise du soir apres coupure en pleine chauffe, mesure ECS sur 10 min ; recharge reseau HC : courant calcule (12 kVA) et consigne non tenue ; HC de midi : AUTO ne chauffe pas (BOOST), alerte v2, Jeep (contact, relecture, zone), recharge batterie bridee a 30 A par l'ACM ; jour faible = EDF les deux fenetres HC pour le chauffe-eau, alerte BOOST, Victron Mobile refait ; 1re decision jour faible en reel, mesure ECS reparee, douches 06:00/20:00, Grafana chauffe-eau, tableaux ECS ; chauffe-eau en HC de nuit les jours faibles (decision du bilan, workflow ECS sous controle du depot, VRM, V2.3 desactive) ; garde batterie reseau : voiture bridee a 6 A au lieu d'un arret ; garde-fou concierge 2 h ; allers-retours brefs de la borne retrouves dans l'historique HA (echange VW -> JEEP de 20 s), production 14,5 kWh contre 22 prevus, defaut garde batterie en mode reseau a trancher ; voiture a midi : la batterie n'est plus comptee comme du solaire (EDF automatique les jours de depart si le surplus ne couvre pas) ; nuit 05->06 : JEEP chargee sur EDF en complete (6,32 kWh), budget 0 du a la prevision, Windy/ECMWF contre VRM, a comparer ce soir ; concierge aligne sur la preuve fin de trajet ; a verifier : nuit 05->06 et mardi faible, comparaison VRM/Forecast.Solar ; JEEP identifiee aussi par sa fin de trajet, prevision de la nuit 05->06 ; identification JEEP reussie en reel, correction humaine qui prime (refus supprime), session du 04/10 coupee ; EVCS Master en quatre onglets ; Jeep lue par Uconnect et identifiee seule, capacite JEEP 10 kWh, concierge variante (a) ; releve par photo (SPARK), hass-uconnect en attente ; ecart semaine/week-end mesure (pas de profil week-end la nuit) ; nuit du budget : budget borne au plancher prudent, sans EDF = voiture bloquee meme EDF ferme (batterie seule) ; Energie : sauts corriges ; tableau Energie complete (Sud/Ouest, batterie par integration, borne ; aucun redemarrage HA) ; kilometrage des vehicules (km/kWh, capacite, releves de reference, agent cable 15/15) ; a verifier le 04/10 au matin : nuit du budget JEEP ; concierge : vehicule non confirme demande, heure de saisie ; Grafana vehicules en tableau ; Grafana vehicules : ZERO et Invite ajoutes, session 16:46 reetiquetee ZERO ; prevision SOC 10 h ~35 % ; concierge fiable : niveau note par le script, une ligne, dedoublonnage, simulation par drapeau ; agent : message incompris = question, bourse sur demande explicite (banc final 6/6) ; a verifier : nuit du samedi 03 au dimanche 04/10 (budget < besoin JEEP), cron orange-cloture lundi 05/10 apres 17:40, comparaison VRM / Forecast.Solar lundi 05/10*
|
||||
|
||||
8
packages/evcs_piles.yaml
Executable file
8
packages/evcs_piles.yaml
Executable file
@@ -0,0 +1,8 @@
|
||||
# Piles des capteurs (10/10/2026). Le service d'historisation publie
|
||||
# sensor.evcs_piles ; SPARK previent par WhatsApp. Liste d'entites a ignorer
|
||||
# (separees par des virgules), par exemple un appareil d'une autre maison.
|
||||
input_text:
|
||||
evcs_piles_ignorees:
|
||||
name: Piles - capteurs a ignorer
|
||||
max: 255
|
||||
icon: mdi:battery-off-outline
|
||||
159
src/sdb/BUILD_ACTIONS.js
Normal file
159
src/sdb/BUILD_ACTIONS.js
Normal file
@@ -0,0 +1,159 @@
|
||||
const root = $input.first().json || {};
|
||||
const calls = root.ha_service_calls || {};
|
||||
const decision = root.decision || {};
|
||||
const actions = [];
|
||||
|
||||
function pushAction(action, call, enabled = true) {
|
||||
if (!enabled) return;
|
||||
if (!call) return;
|
||||
if (call.execute === false) return;
|
||||
|
||||
const { execute, ...rest } = call;
|
||||
|
||||
actions.push({
|
||||
json: {
|
||||
action,
|
||||
...rest
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// HELPERS TOUJOURS MIS A JOUR
|
||||
// =====================================================
|
||||
pushAction('helper_state', calls.helper_state, true);
|
||||
pushAction('helper_reason', calls.helper_reason, true);
|
||||
|
||||
// =====================================================
|
||||
// OFF ENFANTS D'ABORD
|
||||
// =====================================================
|
||||
pushAction(
|
||||
'deshumide',
|
||||
calls.deshumide,
|
||||
decision.deshumide_action === 'turn_off'
|
||||
);
|
||||
|
||||
pushAction(
|
||||
'radiator',
|
||||
calls.radiator,
|
||||
decision.radiator_action === 'turn_off'
|
||||
);
|
||||
|
||||
// =====================================================
|
||||
// PUIS OFF MASTER SI NECESSAIRE
|
||||
// =====================================================
|
||||
pushAction(
|
||||
'master_power',
|
||||
calls.master_power,
|
||||
decision.master_action === 'turn_off'
|
||||
);
|
||||
|
||||
// =====================================================
|
||||
// ON MASTER D'ABORD
|
||||
// =====================================================
|
||||
pushAction(
|
||||
'master_power',
|
||||
calls.master_power,
|
||||
decision.master_action === 'turn_on'
|
||||
);
|
||||
|
||||
// =====================================================
|
||||
// PUIS ON ENFANTS
|
||||
// =====================================================
|
||||
pushAction(
|
||||
'deshumide',
|
||||
calls.deshumide,
|
||||
decision.deshumide_action === 'turn_on'
|
||||
);
|
||||
|
||||
pushAction(
|
||||
'radiator',
|
||||
calls.radiator,
|
||||
decision.radiator_action === 'turn_on'
|
||||
);
|
||||
|
||||
// =====================================================
|
||||
// LATCHES
|
||||
// =====================================================
|
||||
pushAction(
|
||||
'helper_master_latch',
|
||||
calls.helper_master_latch,
|
||||
decision.master_latch_action !== 'none'
|
||||
);
|
||||
|
||||
pushAction(
|
||||
'helper_deshumide_latch',
|
||||
calls.helper_deshumide_latch,
|
||||
decision.deshumide_latch_action !== 'none'
|
||||
);
|
||||
|
||||
pushAction(
|
||||
'helper_radiator_latch',
|
||||
calls.helper_radiator_latch,
|
||||
decision.radiator_latch_action !== 'none'
|
||||
);
|
||||
|
||||
// =====================================================
|
||||
// TIMESTAMPS
|
||||
// =====================================================
|
||||
pushAction(
|
||||
'helper_master_last_on_at',
|
||||
calls.helper_master_last_on_at,
|
||||
!!decision.set_master_last_on_at
|
||||
);
|
||||
|
||||
pushAction(
|
||||
'helper_master_last_off_at',
|
||||
calls.helper_master_last_off_at,
|
||||
!!decision.set_master_last_off_at
|
||||
);
|
||||
|
||||
pushAction(
|
||||
'helper_deshumide_last_on_at',
|
||||
calls.helper_deshumide_last_on_at,
|
||||
!!decision.set_deshumide_last_on_at
|
||||
);
|
||||
|
||||
pushAction(
|
||||
'helper_deshumide_last_off_at',
|
||||
calls.helper_deshumide_last_off_at,
|
||||
!!decision.set_deshumide_last_off_at
|
||||
);
|
||||
|
||||
pushAction(
|
||||
'helper_radiator_last_on_at',
|
||||
calls.helper_radiator_last_on_at,
|
||||
!!decision.set_radiator_last_on_at
|
||||
);
|
||||
|
||||
pushAction(
|
||||
'helper_radiator_last_off_at',
|
||||
calls.helper_radiator_last_off_at,
|
||||
!!decision.set_radiator_last_off_at
|
||||
);
|
||||
|
||||
// =====================================================
|
||||
// SIGNATURE + LOGBOOK SI CHANGEMENT
|
||||
// =====================================================
|
||||
pushAction(
|
||||
'helper_signature',
|
||||
calls.helper_signature,
|
||||
!!decision.signature_changed
|
||||
);
|
||||
|
||||
pushAction(
|
||||
'logbook',
|
||||
calls.logbook,
|
||||
!!decision.signature_changed
|
||||
);
|
||||
|
||||
// =====================================================
|
||||
// NOTIFICATION
|
||||
// =====================================================
|
||||
pushAction(
|
||||
'mobile_notification',
|
||||
calls.mobile_notification,
|
||||
!!decision.should_notify
|
||||
);
|
||||
|
||||
return actions;
|
||||
1058
src/sdb/COMPUTE.js
Normal file
1058
src/sdb/COMPUTE.js
Normal file
File diff suppressed because it is too large
Load Diff
68
src/sdb/HA_SNAPSHOT_TEMPLATE.json.tpl
Normal file
68
src/sdb/HA_SNAPSHOT_TEMPLATE.json.tpl
Normal file
@@ -0,0 +1,68 @@
|
||||
{{ {
|
||||
"soc_percent": states("sensor.victron_battery_soc"),
|
||||
"battery_current_a": states("sensor.victron_battery_current"),
|
||||
"pv_power_w": states("sensor.victron_pv_power_system"),
|
||||
"pv_power_10m_avg_w": states("sensor.victron_pv_power_10m_avg"),
|
||||
"house_ac_power_w": states("sensor.victron_consumption_power_l1"),
|
||||
"grid_power_w": states("sensor.victron_grid_power_l1"),
|
||||
"grid_state": states("switch.edf_general_ok_switch"),
|
||||
"victron_acin1_available": states("sensor.victron_ac_in1_available"),
|
||||
|
||||
"sdb_master_power": states("switch.sdb_prise_socket_1"),
|
||||
"sdb_master_energy_total": states("sensor.sdb_prise_energie_totale"),
|
||||
|
||||
"deshumide_switch": states("switch.deshumide_sdb_socket_1"),
|
||||
"deshumide_energy_total": states("sensor.deshumide_sdb_energie_totale"),
|
||||
|
||||
"radiator_switch": states("switch.sdb_radiateur_socket_1"),
|
||||
"radiator_energy_total": states("sensor.sdb_radiateur_energie_totale"),
|
||||
|
||||
"sdb_temperature_c": states("sensor.sdb_temp_hygro_temperature"),
|
||||
"sdb_humidity_pct": states("sensor.sdb_temp_hygro_humidite"),
|
||||
"sdb_humidity_age_min": ((as_timestamp(now()) - as_timestamp(states.sensor.sdb_temp_hygro_humidite.last_reported or states.sensor.sdb_temp_hygro_humidite.last_changed)) / 60) | round(0) if states.sensor.sdb_temp_hygro_humidite is not none else -1,
|
||||
"balance_required_soc_prudent": states("input_number.evcs_balance_required_soc_prudent"),
|
||||
"balance_status": states("input_text.evcs_balance_status"),
|
||||
|
||||
"auto_enabled": states("input_boolean.sdb_climate_surplus_auto_enabled"),
|
||||
"notifications_enabled": states("input_boolean.sdb_climate_surplus_notifications"),
|
||||
"manual_adopt_enabled": states("input_boolean.sdb_master_manual_adopt_enabled"),
|
||||
|
||||
"master_power_latch": states("input_boolean.sdb_master_power_latch"),
|
||||
"deshumide_latch": states("input_boolean.sdb_deshumide_latch"),
|
||||
"radiator_latch": states("input_boolean.sdb_radiator_latch"),
|
||||
|
||||
"temp_on_min_c": states("input_number.sdb_temp_on_min_c"),
|
||||
"temp_off_max_c": states("input_number.sdb_temp_off_max_c"),
|
||||
"humidity_on_pct": states("input_number.sdb_humidity_on_pct"),
|
||||
"humidity_off_pct": states("input_number.sdb_humidity_off_pct"),
|
||||
|
||||
"deshumide_min_on_minutes": states("input_number.sdb_deshumide_min_on_minutes"),
|
||||
"deshumide_min_off_minutes": states("input_number.sdb_deshumide_min_off_minutes"),
|
||||
|
||||
"radiator_auto_enabled": states("input_boolean.sdb_radiator_surplus_auto_enabled"),
|
||||
"radiator_soc_on": states("input_number.sdb_radiator_soc_on"),
|
||||
"radiator_soc_off": states("input_number.sdb_radiator_soc_off"),
|
||||
"radiator_pv_on_w": states("input_number.sdb_radiator_pv_on_w"),
|
||||
"radiator_pv_off_w": states("input_number.sdb_radiator_pv_off_w"),
|
||||
"radiator_battery_charge_ceiling_a": states("input_number.sdb_radiator_battery_charge_ceiling_a"),
|
||||
"radiator_house_ac_max_w": states("input_number.sdb_radiator_house_ac_max_w"),
|
||||
"radiator_min_on_minutes": states("input_number.sdb_radiator_min_on_minutes"),
|
||||
"radiator_min_off_minutes": states("input_number.sdb_radiator_min_off_minutes"),
|
||||
|
||||
"day_start_time": states("input_datetime.victron_flexible_day_start_time"),
|
||||
"day_stop_time": states("input_datetime.victron_flexible_day_stop_time"),
|
||||
|
||||
"master_last_on_at": states("input_datetime.sdb_master_last_on_at"),
|
||||
"master_last_off_at": states("input_datetime.sdb_master_last_off_at"),
|
||||
"deshumide_last_on_at": states("input_datetime.sdb_deshumide_last_on_at"),
|
||||
"deshumide_last_off_at": states("input_datetime.sdb_deshumide_last_off_at"),
|
||||
"radiator_last_on_at": states("input_datetime.sdb_radiator_last_on_at"),
|
||||
"radiator_last_off_at": states("input_datetime.sdb_radiator_last_off_at"),
|
||||
|
||||
"evcs_priority_active": states("input_boolean.victron_evcs_priority_active"),
|
||||
"energy_priority_mode": states("input_select.victron_energy_priority_mode"),
|
||||
"energy_priority_reason": states("input_text.victron_energy_priority_reason"),
|
||||
"evcs_priority_until": states("input_datetime.victron_evcs_priority_until"),
|
||||
|
||||
"last_signature": states("input_text.sdb_climate_surplus_last_signature")
|
||||
} | tojson }}
|
||||
28
tests/piles/run.mjs
Normal file
28
tests/piles/run.mjs
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env node
|
||||
import { 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);
|
||||
console.log(f ? `\n${f} ECHEC(S)` : '\nPILES OK'); process.exit(f ? 1 : 0);
|
||||
66
tests/sdb/run.mjs
Normal file
66
tests/sdb/run.mjs
Normal file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env node
|
||||
// Tests du workflow SDB Climate & Surplus (10/10/2026) : vrai code de COMPUTE et
|
||||
// BUILD_ACTIONS en vm, horloge figee au 10/10/2026 00:15 (Paris).
|
||||
import fs from 'fs';
|
||||
import vm from 'vm';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const COMPUTE = fs.readFileSync(path.join(ROOT, 'src/sdb/COMPUTE.js'), 'utf8');
|
||||
const BUILD = fs.readFileSync(path.join(ROOT, 'src/sdb/BUILD_ACTIONS.js'), 'utf8');
|
||||
const FIXE = Date.parse('2026-10-10T00:15:00+02:00');
|
||||
class D extends Date { constructor(...a) { super(...(a.length ? a : [FIXE])); } static now() { return FIXE; } }
|
||||
function run(code, json) {
|
||||
const ctx = { $input: { first: () => ({ json }) }, console, Date: D, Intl, Math, Number, Object, String, JSON, Array, Boolean };
|
||||
vm.createContext(ctx);
|
||||
return vm.runInContext(`(function(){\n${code}\n})()`, ctx);
|
||||
}
|
||||
// Releve reel du 10/10 00:15 : capteur muet depuis le 04/10, deshumidificateur et
|
||||
// prise SDB allumes et memorises, SOC 26 %, EDF ouvert, nuit.
|
||||
const base = {
|
||||
soc_percent: '26', battery_current_a: '-28', pv_power_w: '0', pv_power_10m_avg_w: '0',
|
||||
house_ac_power_w: '1400', grid_power_w: '0', grid_state: 'off', victron_acin1_available: '0',
|
||||
sdb_master_power: 'on', sdb_master_energy_total: '4.1', deshumide_switch: 'on', deshumide_energy_total: '3.0',
|
||||
radiator_switch: 'off', radiator_energy_total: '0',
|
||||
sdb_temperature_c: 'unavailable', sdb_humidity_pct: 'unavailable', sdb_humidity_age_min: '7900',
|
||||
auto_enabled: 'on', notifications_enabled: 'on', manual_adopt_enabled: 'on',
|
||||
master_power_latch: 'on', deshumide_latch: 'on', radiator_latch: 'off',
|
||||
temp_on_min_c: '20', temp_off_max_c: '24', humidity_on_pct: '66', humidity_off_pct: '41',
|
||||
deshumide_min_on_minutes: '15', deshumide_min_off_minutes: '10',
|
||||
radiator_auto_enabled: 'on', radiator_soc_on: '85', radiator_soc_off: '80',
|
||||
master_last_on_at: '2026-10-09 22:39:35', deshumide_last_on_at: '2026-10-09 20:00:35',
|
||||
deshumide_last_off_at: '2026-05-17 02:50:35', master_last_off_at: '2026-05-17 05:45:35',
|
||||
balance_required_soc_prudent: '60', balance_status: 'ok',
|
||||
};
|
||||
const dec = (o) => run(COMPUTE, { data: JSON.stringify({ ...base, ...o }) })[0].json.decision;
|
||||
let f = 0;
|
||||
const ok = (l, c, d = '') => { if (c) console.log(`✅ ${l}`); else { f++; console.log(`❌ ${l} ${d}`); } };
|
||||
|
||||
let d = dec({});
|
||||
ok('muet-releve-1010-coupe', d.deshumide_action === 'turn_off' && d.master_action === 'turn_off' && d.deshumide_latch_action === 'turn_off', JSON.stringify(d));
|
||||
d = dec({ deshumide_last_on_at: '2026-10-10 00:10:00' });
|
||||
ok('muet-temps-mini-respecte', d.deshumide_action === 'none', JSON.stringify(d));
|
||||
d = dec({ deshumide_switch: 'off', deshumide_latch: 'off', master_power_latch: 'off', sdb_master_power: 'off' });
|
||||
ok('muet-ne-demarre-pas', d.deshumide_action === 'none' && d.master_action === 'none', JSON.stringify(d));
|
||||
const vivant = { sdb_temperature_c: '26.3', sdb_humidity_pct: '70', sdb_humidity_age_min: '2', deshumide_switch: 'off', deshumide_latch: 'off', master_power_latch: 'off', sdb_master_power: 'off' };
|
||||
d = dec(vivant);
|
||||
ok('batterie-reservee-pas-de-demarrage', d.deshumide_action === 'none', JSON.stringify(d));
|
||||
d = dec({ ...vivant, grid_state: 'on' });
|
||||
ok('edf-ferme-demarre', d.deshumide_action === 'turn_on' && d.master_action === 'turn_on', JSON.stringify(d));
|
||||
d = dec({ ...vivant, soc_percent: '80' });
|
||||
ok('batterie-au-dessus-du-bilan-demarre', d.deshumide_action === 'turn_on', JSON.stringify(d));
|
||||
d = dec({ ...vivant, pv_power_10m_avg_w: '2500', house_ac_power_w: '1200' });
|
||||
ok('surplus-solaire-demarre', d.deshumide_action === 'turn_on', JSON.stringify(d));
|
||||
d = dec({ ...vivant, balance_status: 'degrade' });
|
||||
ok('bilan-illisible-comportement-anterieur', d.deshumide_action === 'turn_on', JSON.stringify(d));
|
||||
d = dec({ sdb_temperature_c: '26.3', sdb_humidity_pct: '56', sdb_humidity_age_min: '2' });
|
||||
ok('batterie-reservee-coupe-apres-temps-mini', d.deshumide_action === 'turn_off', JSON.stringify(d));
|
||||
d = dec({ sdb_temperature_c: '26.3', sdb_humidity_pct: '56', sdb_humidity_age_min: '2', soc_percent: '80' });
|
||||
ok('hysteresis-humidite-inchangee', d.deshumide_action === 'none', JSON.stringify(d));
|
||||
d = dec({ sdb_temperature_c: '26.3', sdb_humidity_pct: '56', sdb_humidity_age_min: '400', soc_percent: '80' });
|
||||
ok('valeur-figee-3h-muet', d.deshumide_action === 'turn_off', JSON.stringify(d));
|
||||
const out = run(COMPUTE, { data: JSON.stringify(base) })[0].json;
|
||||
const acts = run(BUILD, out).map((x) => x.json.action);
|
||||
ok('build-coupe-enfant-puis-master', acts.indexOf('deshumide') >= 0 && acts.indexOf('deshumide') < acts.indexOf('master_power'), acts.join(','));
|
||||
console.log(f ? `\n${f} ECHEC(S)` : '\nSDB OK');
|
||||
process.exit(f ? 1 : 0);
|
||||
@@ -33,6 +33,7 @@ import { creerSchemaKm, ingererReleves, ingererNiveauSaisi, ingererJeep } from '
|
||||
import { texteRapport, envoyer, donneesNuit, texteDepuis } from './rapport-nuit.mjs';
|
||||
import { donneesRevue, constatsRevue } from './revue-jour.mjs';
|
||||
import { detecterSituation, lireChoix, DUREE_MIN as ARBITRAGE_DUREE_MIN } from './arbitrage.mjs';
|
||||
import { pilesAAlerter } from './piles.mjs';
|
||||
import { diagnosticPv, etatCellules, productionJour, chargeSolaire, bilanNuit, etatReseau, importJour, minuitLocal, COMPTEURS_IMPORT, COMPTEUR_EDF, consoJour, COMPTEURS_CONSO, projectionSoir, previsionNuit, vrmVersCourbes, nuitMaisonPrevue } from './diagnostics-agent.mjs';
|
||||
import { transitionsDansSerie } from './allers-retours.mjs';
|
||||
import { energieSur24h, energieDepuis, COMPTEUR_CHAUFFE_EAU } from './chauffe-eau.mjs';
|
||||
@@ -2064,6 +2065,28 @@ async function appliquerOrdreAirtria(sql, client, st) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// PILES DES CAPTEURS (10/10/2026, tools/piles.mjs) : toutes les 15 min,
|
||||
// sensor.evcs_piles = nombre d'alertes ; attributs alertes [{entite, nom,
|
||||
// statut faible|muet, niveau, depuis, texte}] et texte. SPARK previent par
|
||||
// WhatsApp. input_text.evcs_piles_ignorees (liste separee par des virgules)
|
||||
// pour faire taire un appareil.
|
||||
let pilesPubliees = false;
|
||||
async function publierPiles() {
|
||||
const r = await fetch(`${HA_URL}/api/states`, { headers: { Authorization: `Bearer ${HA_TOKEN}` }, signal: AbortSignal.timeout(15000) });
|
||||
if (!r.ok) throw new Error(`HA ${r.status}`);
|
||||
const etats = await r.json();
|
||||
const ign = String(etats.find((x) => x.entity_id === 'input_text.evcs_piles_ignorees')?.state || '')
|
||||
.split(',').filter((x) => x && x !== 'unknown');
|
||||
const p = pilesAAlerter(etats, Date.now(), ign);
|
||||
await publierCapteur('sensor.evcs_piles', p.alertes.length, {
|
||||
friendly_name: 'Piles des capteurs a changer', icon: 'mdi:battery-alert-variant-outline',
|
||||
alertes: p.alertes, texte: p.texte, ignorees: ign,
|
||||
});
|
||||
pilesPubliees = true;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// ARBITRE ENCADRE (08/10/2026, tools/arbitrage.mjs)
|
||||
// ---------------------------------------------------------------------
|
||||
@@ -2633,6 +2656,9 @@ async function cycle(client) {
|
||||
rapport: DRY ? 0 : await envoyerRapportDuMatin(sql, client).catch((e) => {
|
||||
console.error('rapport de nuit :', e.message); return 0;
|
||||
}),
|
||||
piles: (DRY || (pilesPubliees && new Date().getMinutes() % 15 !== 6)) ? 0 : await publierPiles().catch((e) => {
|
||||
console.error('piles :', e.message); return 0;
|
||||
}),
|
||||
arbitrage: DRY ? 0 : await arbitrer(sql, client, st).catch((e) => {
|
||||
console.error('arbitrage :', e.message); return 0;
|
||||
}),
|
||||
|
||||
@@ -28,6 +28,17 @@ export const WORKFLOWS = [
|
||||
{ file: 'HA_SNAPSHOT_TEMPLATE.json.tpl', node: 'HA Snapshot Advisor', kind: 'template' },
|
||||
],
|
||||
},
|
||||
{
|
||||
// SDB Climate & Surplus Manager V2.2 (2NhrfxuGm4pTUNs9), sous controle du depot le 10/10/2026.
|
||||
id: 'sdb',
|
||||
workflowFile: 'workflows/sdb-climate-surplus-v2.2.json',
|
||||
srcDir: 'src/sdb',
|
||||
nodes: [
|
||||
{ file: 'COMPUTE.js', node: 'Compute SDB Climate & Surplus V2.2', kind: 'jsCode' },
|
||||
{ file: 'BUILD_ACTIONS.js', node: 'Build SDB Climate & Surplus Actions V2.2', kind: 'jsCode' },
|
||||
{ file: 'HA_SNAPSHOT_TEMPLATE.json.tpl', node: 'HA Snapshot SDB Climate & Surplus', kind: 'template' },
|
||||
],
|
||||
},
|
||||
{
|
||||
// Dialogue avec le conseiller dans Home Assistant (08/10/2026).
|
||||
id: 'conseiller-dialogue',
|
||||
|
||||
@@ -109,6 +109,7 @@ console.log('Syntaxe OK sur tous les nodes jsCode.');
|
||||
const isEvcsWorkflow = path.basename(INPUT) === 'evcs-surplus-controller-v2.6.json';
|
||||
const isAdvisorWorkflow = path.basename(INPUT) === 'evcs-advisor-shadow-v1.json';
|
||||
const isDialogueWorkflow = path.basename(INPUT) === 'evcs-conseiller-dialogue-v1.json';
|
||||
const isSdbWorkflow = path.basename(INPUT) === 'sdb-climate-surplus-v2.2.json';
|
||||
const isHysteresisWorkflow = path.basename(INPUT) === 'victron-grid-hysteresis-v1.json';
|
||||
const isAcmWorkflow = path.basename(INPUT) === 'victron-auto-charge-manager-v1.2.json';
|
||||
const isWaterHeaterWorkflow = path.basename(INPUT) === 'water-heater-ecs-v1.json';
|
||||
@@ -120,6 +121,7 @@ const testSuite =
|
||||
isEvcsWorkflow && fs.existsSync('tests/golden/run.mjs') ? 'tests/golden/run.mjs' :
|
||||
isAdvisorWorkflow && fs.existsSync('tests/advisor/run.mjs') ? 'tests/advisor/run.mjs' :
|
||||
isDialogueWorkflow && fs.existsSync('tests/conseiller-dialogue/run.mjs') ? 'tests/conseiller-dialogue/run.mjs' :
|
||||
isSdbWorkflow && fs.existsSync('tests/sdb/run.mjs') ? 'tests/sdb/run.mjs' :
|
||||
isHysteresisWorkflow && fs.existsSync('tests/golden-hysteresis/run.mjs') ? 'tests/golden-hysteresis/run.mjs' :
|
||||
isAcmWorkflow && fs.existsSync('tests/golden-acm/run.mjs') ? 'tests/golden-acm/run.mjs' :
|
||||
isBalanceWorkflow && fs.existsSync('tests/energy-balance/run.mjs') ? 'tests/energy-balance/run.mjs' :
|
||||
|
||||
52
tools/piles.mjs
Normal file
52
tools/piles.mjs
Normal file
@@ -0,0 +1,52 @@
|
||||
// =====================================================================
|
||||
// PILES DES CAPTEURS (10/10/2026, demande du proprietaire)
|
||||
// Le capteur temperature/humidite de la SDB est reste muet six jours (piles)
|
||||
// sans que personne ne le sache. Ce module repere, parmi les etats HA, les
|
||||
// capteurs d'APPAREILS a pile dont la pile est faible ou qui ne publient plus ;
|
||||
// le service d'historisation publie sensor.evcs_piles et SPARK previent par
|
||||
// WhatsApp. Fonction pure (tests : tests/piles/run.mjs).
|
||||
// =====================================================================
|
||||
|
||||
export const SEUIL_PCT = 20; // pile faible a 20 % et moins
|
||||
export const MUET_H = 24; // indisponible depuis plus de 24 h
|
||||
// Pas des piles de capteurs : parc Victron, voiture, telephones, reglages GX.
|
||||
const EXCLUS = /^(sensor|number|binary_sensor)\.(victron_|gx_device_|airtria_8_|jeep_|iphone_|ipad_|samsung_)/;
|
||||
|
||||
/**
|
||||
* @param {Array} etats liste des etats HA ({entity_id, state, attributes, last_changed})
|
||||
* @param {number} maintenantMs
|
||||
* @param {string[]} ignorees entites que le proprietaire a demande d'ignorer
|
||||
*/
|
||||
export function pilesAAlerter(etats, maintenantMs, ignorees = []) {
|
||||
const ign = new Set(ignorees.map((x) => x.trim()).filter(Boolean));
|
||||
const alertes = [];
|
||||
for (const s of etats) {
|
||||
const id = s.entity_id;
|
||||
if (!id.startsWith('sensor.') || EXCLUS.test(id) || ign.has(id)) continue;
|
||||
const a = s.attributes || {};
|
||||
const enPct = a.device_class === 'battery' && (a.unit_of_measurement === '%' || a.unit_of_measurement === undefined);
|
||||
const enEnum = /_etat_batterie$/.test(id);
|
||||
if (!enPct && !enEnum) continue;
|
||||
const nom = String(a.friendly_name || id).replace(/\s+/g, ' ').trim();
|
||||
const v = String(s.state ?? '').trim().toLowerCase();
|
||||
const depuisMs = Date.parse(s.last_changed);
|
||||
if (v === 'unavailable' || v === 'unknown' || v === '') {
|
||||
const h = Number.isFinite(depuisMs) ? (maintenantMs - depuisMs) / 3600000 : Infinity;
|
||||
if (h > MUET_H) alertes.push({ entite: id, nom, statut: 'muet', niveau: null, depuis: s.last_changed,
|
||||
texte: `${nom} ne publie plus depuis ${Number.isFinite(h) ? Math.round(h / 24) + ' j' : 'longtemps'} (pile vide ou appareil hors ligne)` });
|
||||
continue;
|
||||
}
|
||||
if (enPct) {
|
||||
const n = Number(v);
|
||||
if (Number.isFinite(n) && n <= SEUIL_PCT) alertes.push({ entite: id, nom, statut: 'faible', niveau: n, depuis: s.last_changed,
|
||||
texte: `${nom} : pile a ${n} %` });
|
||||
} else if (v === 'low') {
|
||||
alertes.push({ entite: id, nom, statut: 'faible', niveau: 'low', depuis: s.last_changed, texte: `${nom} : pile faible` });
|
||||
}
|
||||
}
|
||||
alertes.sort((x, y) => (x.statut === y.statut ? x.nom.localeCompare(y.nom) : x.statut === 'faible' ? -1 : 1));
|
||||
return {
|
||||
alertes,
|
||||
texte: alertes.length ? alertes.map((x) => `- ${x.texte}`).join('\n') : 'Toutes les piles des capteurs sont bonnes.',
|
||||
};
|
||||
}
|
||||
467
workflows/sdb-climate-surplus-v2.2.json
Normal file
467
workflows/sdb-climate-surplus-v2.2.json
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user