14b3967590
- Topics individuels par capteur sous un topic de base configurable (défaut: solar/) PV, batterie (tension/SOC/temp/statut), load, énergie, soleil, RS485, relais, entrées DI - Abonnement relay/1/set et relay/2/set pour piloter les relais depuis MQTT - Config NVS : serveur, port, user/pass optionnel, topic base, intervalle (défaut 30s) - Reconnexion automatique toutes les 15s si broker inaccessible - Publication immédiate après connexion et après changement de config - Route GET/POST /api/mqtt + UI onglet Config avec liste des topics générée dynamiquement - Stubs QEMU (#ifndef QEMU_BUILD) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1158 lines
43 KiB
JavaScript
1158 lines
43 KiB
JavaScript
'use strict';
|
||
|
||
const REFRESH_MS = (parseInt(localStorage.getItem('refresh_ms') || '1000', 10));
|
||
|
||
// Noms configurables — chargés depuis /api/names au démarrage
|
||
const noms = { relay1: 'Relais 1', relay2: 'Relais 2', di1: 'Entrée 1', di2: 'Entrée 2' };
|
||
|
||
async function chargerNoms() {
|
||
try {
|
||
const d = await (await fetch('/api/names')).json();
|
||
if (d.relay1) noms.relay1 = d.relay1;
|
||
if (d.relay2) noms.relay2 = d.relay2;
|
||
if (d.di1) noms.di1 = d.di1;
|
||
if (d.di2) noms.di2 = d.di2;
|
||
} catch {}
|
||
appliquerNoms();
|
||
}
|
||
|
||
function appliquerNoms() {
|
||
setText('label-relay1', noms.relay1);
|
||
setText('label-relay2', noms.relay2);
|
||
setText('label-di1', noms.di1);
|
||
setText('label-di2', noms.di2);
|
||
setText('cmd-label-r1', noms.relay1);
|
||
setText('cmd-label-r2', noms.relay2);
|
||
const r1 = document.getElementById('c-n-relay1'); if (r1) r1.value = noms.relay1;
|
||
const r2 = document.getElementById('c-n-relay2'); if (r2) r2.value = noms.relay2;
|
||
const d1 = document.getElementById('c-n-di1'); if (d1) d1.value = noms.di1;
|
||
const d2 = document.getElementById('c-n-di2'); if (d2) d2.value = noms.di2;
|
||
}
|
||
|
||
async function sauvegarderNoms() {
|
||
const payload = {
|
||
relay1: document.getElementById('c-n-relay1').value.trim() || 'Relais 1',
|
||
relay2: document.getElementById('c-n-relay2').value.trim() || 'Relais 2',
|
||
di1: document.getElementById('c-n-di1').value.trim() || 'Entrée 1',
|
||
di2: document.getElementById('c-n-di2').value.trim() || 'Entrée 2',
|
||
};
|
||
await fetch('/api/names', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload)
|
||
});
|
||
location.reload();
|
||
}
|
||
|
||
// --- Navigation onglets ---
|
||
function afficherOnglet(nom, bouton) {
|
||
document.querySelectorAll('.onglet').forEach(s => s.classList.remove('actif'));
|
||
document.querySelectorAll('.tab').forEach(b => b.classList.remove('active'));
|
||
document.getElementById(nom).classList.add('actif');
|
||
bouton.classList.add('active');
|
||
if (nom === 'regles') chargerRegles();
|
||
if (nom === 'config') { chargerSleep(); chargerWifi(); chargerPrefsUI(); chargerModbus(); chargerMqtt(); chargerWireGuard(); }
|
||
if (nom === 'historique') chargerHistorique();
|
||
if (nom === 'debug') chargerDebug();
|
||
if (nom === 'epever-config') lireConfigEpever();
|
||
}
|
||
|
||
// --- Rafraîchissement de l'état ---
|
||
async function rafraichir() {
|
||
try {
|
||
const res = await fetch('/api/state');
|
||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||
const d = await res.json();
|
||
mettreAJourUI(d);
|
||
} catch {
|
||
const b = document.getElementById('rs485-badge');
|
||
b.textContent = 'Hors ligne';
|
||
b.className = 'badge badge-err';
|
||
}
|
||
}
|
||
|
||
function mettreAJourUI(d) {
|
||
// PV & batterie
|
||
setText('battery', d.battery.toFixed(2) + ' V');
|
||
setText('pv', d.pv.toFixed(2) + ' V');
|
||
setText('pvCurrent', d.pvCurrent.toFixed(2) + ' A');
|
||
setText('sun', d.sun ? '☀ Jour' : '🌙 Nuit');
|
||
setText('epeverTime', d.epeverTime || '--');
|
||
setText('epeverClockOk', d.epeverClockOk ? 'OK' : 'ERR');
|
||
setText('header-clock', d.espClockOk ? d.espTime : (d.epeverTime || '--'));
|
||
// Batterie détaillée
|
||
setText('batSOC', d.batSOC + ' %');
|
||
setText('batTemp', d.batTemperature.toFixed(1) + ' °C');
|
||
setText('batStatut', ['Arrêt','Float','Boost','Égalisation'][d.batStatut] || '--');
|
||
// Load & énergie
|
||
setText('loadVoltage', d.loadVoltage.toFixed(2) + ' V');
|
||
setText('loadCurrent', d.loadCurrent.toFixed(2) + ' A');
|
||
setText('loadPower', d.loadPower.toFixed(1) + ' W');
|
||
setText('energieGenJour', d.energieGenJour.toFixed(2) + ' kWh');
|
||
setText('energieConJour', d.energieConJour.toFixed(2) + ' kWh');
|
||
setText('energieGenTotal', d.energieGenTotal.toFixed(2) + ' kWh');
|
||
setText('energieConTotal', d.energieConTotal.toFixed(2) + ' kWh');
|
||
// Relais & boutons
|
||
setRelais('relay1-etat', 'carte-relay1', 'led-r1', 'btn-r1-on', 'btn-r1-off', d.relay1);
|
||
setRelais('relay2-etat', 'carte-relay2', 'led-r2', 'btn-r2-on', 'btn-r2-off', d.relay2);
|
||
setBouton('di1-etat', d.di1);
|
||
setBouton('di2-etat', d.di2);
|
||
|
||
const badge = document.getElementById('rs485-badge');
|
||
badge.textContent = d.rs485_ok ? 'RS485 OK' : 'RS485 ERR';
|
||
badge.className = 'badge ' + (d.rs485_ok ? 'badge-ok' : 'badge-err');
|
||
|
||
|
||
document.getElementById('pied-page').textContent =
|
||
'Mise à jour : ' + new Date().toLocaleTimeString('fr-FR');
|
||
}
|
||
|
||
function setBouton(id, etat) {
|
||
const el = document.getElementById(id);
|
||
if (!el) return;
|
||
el.textContent = etat ? '● Appuyé' : '○ Relâché';
|
||
el.className = 'valeur ' + (etat ? 'val-on' : 'val-off');
|
||
}
|
||
|
||
function setRelais(id, carteId, ledId, btnOnId, btnOffId, etat) {
|
||
const el = document.getElementById(id);
|
||
if (el) { el.textContent = etat ? '● ON' : '○ OFF'; el.className = 'valeur ' + (etat ? 'val-on' : 'val-off'); }
|
||
|
||
const carte = document.getElementById(carteId);
|
||
if (carte) carte.classList.toggle('carte-on', etat);
|
||
|
||
const led = document.getElementById(ledId);
|
||
if (led) led.className = 'led ' + (etat ? 'led-on' : 'led-off');
|
||
|
||
const btnOn = document.getElementById(btnOnId);
|
||
const btnOff = document.getElementById(btnOffId);
|
||
if (btnOn) btnOn.className = 'btn btn-vert' + (etat ? ' btn-glow-vert' : ' btn-dim');
|
||
if (btnOff) btnOff.className = 'btn btn-rouge' + (!etat ? ' btn-glow-rouge' : ' btn-dim');
|
||
}
|
||
|
||
function setText(id, val) {
|
||
const el = document.getElementById(id);
|
||
if (el) el.textContent = val;
|
||
}
|
||
|
||
// --- Relais / Mode ---
|
||
async function relay(n, cmd) {
|
||
const res = await fetch('/api/relay/' + n + '/' + cmd, { method: 'POST' });
|
||
if (!res.ok) {
|
||
const d = await res.json().catch(() => ({}));
|
||
afficherToast(d.err || 'Commande refusée');
|
||
}
|
||
rafraichir();
|
||
}
|
||
|
||
function afficherToast(msg) {
|
||
let t = document.getElementById('toast');
|
||
if (!t) {
|
||
t = document.createElement('div');
|
||
t.id = 'toast';
|
||
document.body.appendChild(t);
|
||
}
|
||
t.textContent = msg;
|
||
t.classList.add('visible');
|
||
clearTimeout(t._timer);
|
||
t._timer = setTimeout(() => t.classList.remove('visible'), 2800);
|
||
}
|
||
|
||
async function rebootESP() {
|
||
if (!confirm('Redémarrer l\'ESP32 ?')) return;
|
||
await fetch('/api/reboot', { method: 'POST' });
|
||
afficherToast('Redémarrage en cours…');
|
||
setTimeout(() => location.reload(), 5000);
|
||
}
|
||
|
||
// --- Règles ---
|
||
async function chargerRegles() {
|
||
try {
|
||
const res = await fetch('/api/rules');
|
||
const data = await res.json();
|
||
afficherRegles(data);
|
||
} catch {
|
||
document.getElementById('liste-regles').innerHTML =
|
||
'<p style="color:var(--muted);text-align:center">Erreur chargement</p>';
|
||
}
|
||
}
|
||
|
||
function afficherRegles(regles) {
|
||
const el = document.getElementById('liste-regles');
|
||
if (!regles.length) {
|
||
el.innerHTML = '<p style="color:var(--muted);text-align:center;padding:1rem">Aucune règle</p>';
|
||
return;
|
||
}
|
||
el.innerHTML = regles.map(r => {
|
||
const cond = construireDescription(r);
|
||
const cls = r.enabled ? '' : ' regle-desactivee';
|
||
return `<div class="regle-item${cls}">
|
||
<div class="regle-desc">
|
||
<div class="regle-id">Règle #${r.id}</div>
|
||
${cond}
|
||
</div>
|
||
<button class="btn btn-sm" onclick="toggleRegle(${r.id})">${r.enabled ? 'OFF' : 'ON'}</button>
|
||
<button class="btn btn-sm btn-rouge" onclick="supprimerRegle(${r.id})">✕</button>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
function construireDescription(r) {
|
||
const dec = [];
|
||
if (r.sun !== undefined) dec.push(r.sun ? '☀ Jour' : '🌙 Nuit');
|
||
if (r.di1 !== undefined) dec.push('DI1 ' + (r.di1 ? 'fermé' : 'ouvert'));
|
||
if (r.di2 !== undefined) dec.push('DI2 ' + (r.di2 ? 'fermé' : 'ouvert'));
|
||
|
||
const cond = [];
|
||
if (r.battery_min) cond.push('Bat ≥ ' + r.battery_min + 'V');
|
||
if (r.battery_max) cond.push('Bat ≤ ' + r.battery_max + 'V');
|
||
if (r.pv_min) cond.push('PV ≥ ' + r.pv_min + 'V');
|
||
if (r.pv_max) cond.push('PV ≤ ' + r.pv_max + 'V');
|
||
|
||
const extras = [];
|
||
if (r.delay) extras.push('délai ' + r.delay + 's');
|
||
if (r.hysteresis) extras.push('hyst. ±' + r.hysteresis + 'V');
|
||
|
||
const c = '<span style="color:var(--accent);font-size:0.72rem">▶</span>';
|
||
const lines = [];
|
||
if (dec.length) lines.push(c + ' <em>Si</em> ' + dec.join(' + '));
|
||
if (cond.length) lines.push(c + ' <em>Et</em> ' + cond.join(' + '));
|
||
lines.push(c + ' → Relais ' + r.relay + ' <strong>' + (r.state ? 'ON' : 'OFF') + '</strong>'
|
||
+ (extras.length ? ' <span style="color:var(--muted);font-size:0.78rem">(' + extras.join(', ') + ')</span>' : ''));
|
||
return lines.join('<br>');
|
||
}
|
||
|
||
async function toggleRegle(id) {
|
||
await fetch('/api/rules/toggle?id=' + id, { method: 'POST' });
|
||
chargerRegles();
|
||
}
|
||
|
||
async function supprimerRegle(id) {
|
||
await fetch('/api/rules/delete?id=' + id, { method: 'POST' });
|
||
chargerRegles();
|
||
}
|
||
|
||
async function ajouterRegle() {
|
||
const sun = document.getElementById('f-sun').value;
|
||
const di1 = document.getElementById('f-di1').value;
|
||
const di2 = document.getElementById('f-di2').value;
|
||
const batMin = parseFloat(document.getElementById('f-batmin').value) || 0;
|
||
const batMax = parseFloat(document.getElementById('f-batmax').value) || 0;
|
||
const pvMin = parseFloat(document.getElementById('f-pvmin').value) || 0;
|
||
const pvMax = parseFloat(document.getElementById('f-pvmax').value) || 0;
|
||
const relay = parseInt(document.getElementById('f-relay').value);
|
||
const state = document.getElementById('f-state').value === 'true';
|
||
const delay = parseInt(document.getElementById('f-delay').value) || 0;
|
||
const hyst = parseFloat(document.getElementById('f-hysteresis').value) || 0;
|
||
|
||
const regle = { enabled: true, relay, state, delay };
|
||
if (sun !== '') regle.sun = sun === 'true';
|
||
if (di1 !== '') regle.di1 = di1 === 'true';
|
||
if (di2 !== '') regle.di2 = di2 === 'true';
|
||
if (batMin > 0) regle.battery_min = batMin;
|
||
if (batMax > 0) regle.battery_max = batMax;
|
||
if (pvMin > 0) regle.pv_min = pvMin;
|
||
if (pvMax > 0) regle.pv_max = pvMax;
|
||
if (hyst > 0) regle.hysteresis = hyst;
|
||
|
||
const res = await fetch('/api/rules', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(regle)
|
||
});
|
||
if (res.ok) chargerRegles();
|
||
}
|
||
|
||
// --- Préférences UI (stockées en localStorage) ---
|
||
function getRefreshMs() {
|
||
return parseInt(localStorage.getItem('refresh_ms') || '1000', 10);
|
||
}
|
||
|
||
function chargerPrefsUI() {
|
||
const r = document.getElementById('c-refresh');
|
||
if (r) r.value = Math.round(getRefreshMs() / 1000);
|
||
const lp = document.getElementById('c-longpress2');
|
||
if (lp) lp.value = getLongPressMs();
|
||
}
|
||
|
||
function sauvegarderInterface() {
|
||
const s = parseInt(document.getElementById('c-refresh').value) || 1;
|
||
const lp = parseInt(document.getElementById('c-longpress2').value) || 500;
|
||
localStorage.setItem('refresh_ms', Math.min(60000, Math.max(1000, s * 1000)));
|
||
localStorage.setItem('longpress_ms', Math.min(3000, Math.max(200, lp)));
|
||
location.reload();
|
||
}
|
||
|
||
// --- WiFi ---
|
||
async function chargerWifi() {
|
||
try {
|
||
const d = await (await fetch('/api/wifi/status')).json();
|
||
|
||
// AP
|
||
setText('wifi-ap-ssid', d.ap?.ssid || '--');
|
||
setText('wifi-ap-ip', d.ap?.ip || '--');
|
||
setText('wifi-ap-clients', (d.ap?.clients ?? '--') + ' client(s)');
|
||
|
||
// Barre de statut
|
||
const bar = document.getElementById('wifi-status-bar');
|
||
if (bar) {
|
||
if (d.sta?.connected) {
|
||
bar.textContent = '✓ Connecté à ' + d.sta.ssid + ' — ' + d.sta.ip;
|
||
bar.className = 'ec-statusbar ec-ok';
|
||
} else if (d.sta?.configured) {
|
||
bar.textContent = '⏳ Tentative de connexion à ' + d.sta.ssid + '…';
|
||
bar.className = 'ec-statusbar';
|
||
} else {
|
||
bar.textContent = 'Mode AP uniquement — aucun réseau configuré';
|
||
bar.className = 'ec-statusbar';
|
||
}
|
||
}
|
||
|
||
// Champs STA pré-remplis
|
||
const ssidInput = document.getElementById('wifi-sta-ssid');
|
||
if (ssidInput && d.sta?.configured && !ssidInput.value)
|
||
ssidInput.value = d.sta.ssid || '';
|
||
|
||
// Bloc info STA connectée
|
||
const infoBloc = document.getElementById('wifi-sta-info');
|
||
if (infoBloc) {
|
||
if (d.sta?.connected) {
|
||
infoBloc.classList.remove('hidden');
|
||
setText('wifi-sta-ssid-cur', d.sta.ssid || '--');
|
||
setText('wifi-sta-ip', d.sta.ip || '--');
|
||
setText('wifi-sta-rssi', (d.sta.rssi ?? '--') + ' dBm');
|
||
} else {
|
||
infoBloc.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
// Bouton Oublier visible seulement si STA configurée
|
||
const btnOublier = document.getElementById('wifi-btn-oublier');
|
||
if (btnOublier)
|
||
btnOublier.style.display = d.sta?.configured ? '' : 'none';
|
||
|
||
} catch { /* silencieux */ }
|
||
chargerMdns();
|
||
}
|
||
|
||
async function scannerWifi() {
|
||
const btn = document.getElementById('wifi-btn-scan');
|
||
const list = document.getElementById('wifi-scan-list');
|
||
if (!btn || !list) return;
|
||
|
||
btn.disabled = true;
|
||
btn.textContent = '…';
|
||
list.innerHTML = '<div class="wifi-scan-item wifi-scan-wait">Scan en cours (3-5s)…</div>';
|
||
list.classList.remove('hidden');
|
||
|
||
try {
|
||
const res = await fetch('/api/wifi/scan');
|
||
const d = await res.json();
|
||
|
||
if (!d.ok || !d.networks?.length) {
|
||
list.innerHTML = '<div class="wifi-scan-item wifi-scan-wait">Aucun réseau trouvé</div>';
|
||
} else {
|
||
// tri par signal décroissant
|
||
d.networks.sort((a, b) => b.rssi - a.rssi);
|
||
list.innerHTML = d.networks.map(n => {
|
||
const bars = n.rssi >= -60 ? '▂▄▆█' : n.rssi >= -75 ? '▂▄▆·' : n.rssi >= -85 ? '▂▄··' : '▂···';
|
||
const lock = n.secured ? ' 🔒' : '';
|
||
return `<div class="wifi-scan-item" onclick="choisirReseau('${n.ssid.replace(/'/g,"\\'")}')">
|
||
<span class="wifi-scan-ssid">${n.ssid}${lock}</span>
|
||
<span class="wifi-scan-rssi">${bars} ${n.rssi}dBm</span>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
} catch(e) {
|
||
list.innerHTML = '<div class="wifi-scan-item wifi-scan-wait">Erreur scan : ' + e.message + '</div>';
|
||
}
|
||
|
||
btn.disabled = false;
|
||
btn.textContent = 'Scanner';
|
||
}
|
||
|
||
function choisirReseau(ssid) {
|
||
const el = document.getElementById('wifi-sta-ssid');
|
||
if (el) el.value = ssid;
|
||
const list = document.getElementById('wifi-scan-list');
|
||
if (list) list.classList.add('hidden');
|
||
const passEl = document.getElementById('wifi-sta-pass');
|
||
if (passEl) passEl.focus();
|
||
}
|
||
|
||
async function connecterWifi() {
|
||
const ssid = (document.getElementById('wifi-sta-ssid')?.value || '').trim();
|
||
const pass = (document.getElementById('wifi-sta-pass')?.value || '');
|
||
if (!ssid) { afficherToast('SSID requis'); return; }
|
||
|
||
const bar = document.getElementById('wifi-status-bar');
|
||
if (bar) { bar.textContent = 'Connexion à ' + ssid + '…'; bar.className = 'ec-statusbar'; }
|
||
|
||
try {
|
||
const res = await fetch('/api/wifi/sta', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ ssid, pass })
|
||
});
|
||
const d = await res.json();
|
||
if (d.ok) {
|
||
afficherToast('Connexion lancée — vérifier le statut dans quelques secondes');
|
||
setTimeout(chargerWifi, 8000); // vérification auto après 8s
|
||
} else {
|
||
afficherToast('⚠ ' + (d.erreur || 'Erreur'));
|
||
}
|
||
} catch(e) {
|
||
afficherToast('Erreur réseau : ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function chargerMdns() {
|
||
try {
|
||
const d = await (await fetch('/api/mdns')).json();
|
||
const el = document.getElementById('wifi-mdns-host');
|
||
if (el && d.hostname) el.value = d.hostname;
|
||
} catch { /* silencieux */ }
|
||
}
|
||
|
||
async function sauvegarderMdns() {
|
||
const el = document.getElementById('wifi-mdns-host');
|
||
const nom = (el?.value || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '');
|
||
if (!nom) { afficherToast('Nom invalide'); return; }
|
||
try {
|
||
const res = await fetch('/api/mdns', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ hostname: nom })
|
||
});
|
||
const d = await res.json();
|
||
if (d.ok) {
|
||
afficherToast('✓ mDNS → ' + nom + '.local');
|
||
if (el) el.value = nom;
|
||
} else {
|
||
afficherToast('⚠ ' + (d.erreur || 'Nom invalide'));
|
||
}
|
||
} catch(e) {
|
||
afficherToast('Erreur : ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function oublierWifi() {
|
||
if (!confirm('Oublier le réseau WiFi configuré et revenir en mode AP uniquement ?')) return;
|
||
try {
|
||
const res = await fetch('/api/wifi/sta/disconnect', { method: 'POST' });
|
||
const d = await res.json();
|
||
if (d.ok) {
|
||
afficherToast('Réseau oublié — mode AP uniquement');
|
||
document.getElementById('wifi-sta-ssid').value = '';
|
||
document.getElementById('wifi-sta-pass').value = '';
|
||
chargerWifi();
|
||
}
|
||
} catch(e) {
|
||
afficherToast('Erreur : ' + e.message);
|
||
}
|
||
}
|
||
|
||
// --- Sleep / Config ---
|
||
async function chargerSleep() {
|
||
try {
|
||
const d = await (await fetch('/api/sleep')).json();
|
||
document.getElementById('c-sleep-actif').value = String(d.actif);
|
||
document.getElementById('c-sleep-intervalle').value = Math.round(d.intervalle / 60);
|
||
document.getElementById('c-sleep-seuil').value = d.seuil;
|
||
} catch { /* silencieux */ }
|
||
}
|
||
|
||
async function sauvegarderSleep() {
|
||
const actif = document.getElementById('c-sleep-actif').value === 'true';
|
||
const intervalle = parseInt(document.getElementById('c-sleep-intervalle').value) * 60;
|
||
const seuil = parseFloat(document.getElementById('c-sleep-seuil').value);
|
||
await fetch('/api/sleep', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ actif, intervalle, seuil })
|
||
});
|
||
}
|
||
|
||
// --- Intervalles Modbus ---
|
||
async function chargerModbus() {
|
||
try {
|
||
const d = await (await fetch('/api/modbus')).json();
|
||
const j = document.getElementById('c-mb-jour'); if (j) j.value = Math.round(d.jour / 1000);
|
||
const n = document.getElementById('c-mb-nuit'); if (n) n.value = Math.round(d.nuit / 1000);
|
||
} catch { /* silencieux */ }
|
||
}
|
||
|
||
async function sauvegarderModbus() {
|
||
const jour = (parseInt(document.getElementById('c-mb-jour').value) || 5) * 1000;
|
||
const nuit = (parseInt(document.getElementById('c-mb-nuit').value) || 30) * 1000;
|
||
await fetch('/api/modbus', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ jour, nuit })
|
||
});
|
||
afficherToast('Intervalles Modbus sauvegardés');
|
||
}
|
||
|
||
// --- Horloge EPEVER ---
|
||
function toDatetimeLocalValue(date) {
|
||
const pad = n => String(n).padStart(2, '0');
|
||
return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()) +
|
||
'T' + pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds());
|
||
}
|
||
|
||
function remplirHeureNavigateur() {
|
||
const input = document.getElementById('c-epever-time');
|
||
if (input) input.value = toDatetimeLocalValue(new Date());
|
||
}
|
||
|
||
async function sauvegarderHeureEpever() {
|
||
const input = document.getElementById('c-epever-time');
|
||
if (!input || !input.value) {
|
||
afficherToast('Date/heure manquante');
|
||
return;
|
||
}
|
||
|
||
const d = new Date(input.value);
|
||
if (Number.isNaN(d.getTime())) {
|
||
afficherToast('Date/heure invalide');
|
||
return;
|
||
}
|
||
|
||
const payload = {
|
||
year: d.getFullYear(),
|
||
month: d.getMonth() + 1,
|
||
day: d.getDate(),
|
||
hour: d.getHours(),
|
||
minute: d.getMinutes(),
|
||
second: d.getSeconds()
|
||
};
|
||
|
||
const res = await fetch('/api/epever/time', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload)
|
||
});
|
||
|
||
afficherToast(res.ok ? 'Horloge EPEVER réglée' : 'Réglage refusé, réessaie dans quelques secondes');
|
||
rafraichir();
|
||
}
|
||
|
||
// --- Debug série / RS485 ---
|
||
async function chargerDebug() {
|
||
const consoleEl = document.getElementById('debug-console');
|
||
const metaEl = document.getElementById('debug-meta');
|
||
if (!consoleEl || !metaEl) return;
|
||
|
||
try {
|
||
const d = await (await fetch('/api/debug/logs')).json();
|
||
metaEl.textContent = d.lines.length + ' lignes en mémoire, compteur ' + d.count;
|
||
consoleEl.textContent = d.lines.map(l => {
|
||
const s = Math.floor((l.t || 0) / 1000);
|
||
return '[' + s.toString().padStart(6, ' ') + 's] ' + l.m;
|
||
}).join('\n') || 'Aucun message.';
|
||
consoleEl.scrollTop = consoleEl.scrollHeight;
|
||
} catch {
|
||
metaEl.textContent = 'Erreur lecture journal';
|
||
consoleEl.textContent = 'Impossible de lire /api/debug/logs';
|
||
}
|
||
}
|
||
|
||
async function viderDebug() {
|
||
await fetch('/api/debug/clear', { method: 'POST' });
|
||
chargerDebug();
|
||
}
|
||
|
||
// --- Historique ---
|
||
let histMode = 'hires';
|
||
|
||
function setHistMode(mode) {
|
||
histMode = mode;
|
||
document.getElementById('btn-hires').classList.toggle('active-mode', mode === 'hires');
|
||
document.getElementById('btn-lores').classList.toggle('active-mode', mode === 'lores');
|
||
chargerHistorique();
|
||
}
|
||
|
||
async function chargerHistorique() {
|
||
try {
|
||
const url = histMode === 'hires' ? '/api/history/hires' : '/api/history';
|
||
const res = await fetch(url);
|
||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||
const d = await res.json();
|
||
await chargerHistoriqueStatus(d);
|
||
const ids = [
|
||
{ id: 'chart-battery', data: d.b, couleur: '#00b894', unite: 'V' },
|
||
{ id: 'chart-pv', data: d.p, couleur: '#e94560', unite: 'V' },
|
||
{ id: 'chart-load', data: d.l, couleur: '#fdcb6e', unite: 'W' },
|
||
{ id: 'chart-soc', data: d.s, couleur: '#74b9ff', unite: '%' },
|
||
];
|
||
ids.forEach(({ id, data, couleur, unite }) => {
|
||
const canvas = document.getElementById(id);
|
||
if (canvas) dessinerGraphe(canvas, data, couleur, unite, d.step);
|
||
});
|
||
afficherDerniersPoints(d);
|
||
} catch (e) {
|
||
setText('hist-debug', 'Erreur chargement historique: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function chargerHistoriqueStatus(hist) {
|
||
try {
|
||
const s = await (await fetch('/api/history/status')).json();
|
||
hist = hist || {};
|
||
const mode = hist.mode || histMode;
|
||
const n = hist.n !== undefined ? hist.n : 0;
|
||
const nbBat = hist.b ? hist.b.length : 0;
|
||
const nbPv = hist.p ? hist.p.length : 0;
|
||
const nbLoad = hist.l ? hist.l.length : 0;
|
||
const nbSoc = hist.s ? hist.s.length : 0;
|
||
const nextH = Math.ceil((s.next_hires_ms || 0) / 1000);
|
||
const nextL = Math.ceil((s.next_lores_ms || 0) / 1000);
|
||
setText('hist-debug',
|
||
'Mode ' + mode +
|
||
' | points affichés: ' + n +
|
||
' | séries b/p/l/s: ' + nbBat + '/' + nbPv + '/' + nbLoad + '/' + nbSoc +
|
||
' | hires: ' + s.hires_n + '/' + s.hires_max +
|
||
' | lores: ' + s.lores_n + '/' + s.lores_max +
|
||
' | acc: ' + s.acc_n +
|
||
' | RS485: ' + (s.rs485_ok ? 'OK' : 'ERR') +
|
||
' | prochain 1min: ' + nextH + 's' +
|
||
' | prochain 5min: ' + nextL + 's'
|
||
);
|
||
} catch {
|
||
setText('hist-debug', 'Debug historique indisponible');
|
||
}
|
||
}
|
||
|
||
function afficherDerniersPoints(hist) {
|
||
const el = document.getElementById('hist-last');
|
||
if (!el) return;
|
||
|
||
const b = (hist.b || []).map(Number);
|
||
const p = (hist.p || []).map(Number);
|
||
const l = (hist.l || []).map(Number);
|
||
const s = (hist.s || []).map(Number);
|
||
const n = Math.min(b.length, p.length, l.length, s.length);
|
||
if (!n) {
|
||
el.textContent = 'Aucun point historique reçu depuis l’API.';
|
||
return;
|
||
}
|
||
|
||
const debut = Math.max(0, n - 5);
|
||
const lignes = [];
|
||
for (let i = debut; i < n; i++) {
|
||
lignes.push(
|
||
'#' + (i + 1) +
|
||
' bat=' + b[i].toFixed(2) + 'V' +
|
||
' pv=' + p[i].toFixed(2) + 'V' +
|
||
' load=' + l[i].toFixed(1) + 'W' +
|
||
' soc=' + s[i].toFixed(0) + '%'
|
||
);
|
||
}
|
||
el.textContent = 'Derniers points: ' + lignes.join(' | ');
|
||
}
|
||
|
||
function dessinerGraphe(canvas, data, couleur, unite, stepSec) {
|
||
stepSec = stepSec || 300;
|
||
data = (data || []).map(Number).filter(Number.isFinite);
|
||
const ctx = canvas.getContext('2d');
|
||
const W = canvas.width, H = canvas.height;
|
||
const pad = { top: 12, right: 8, bottom: 28, left: 38 };
|
||
const w = W - pad.left - pad.right;
|
||
const h = H - pad.top - pad.bottom;
|
||
|
||
ctx.clearRect(0, 0, W, H);
|
||
|
||
if (!data.length) {
|
||
ctx.fillStyle = '#a0aec0';
|
||
ctx.font = '14px system-ui';
|
||
ctx.textAlign = 'center';
|
||
ctx.textBaseline = 'middle';
|
||
ctx.fillText('Pas encore de données', W / 2, H / 2);
|
||
return;
|
||
}
|
||
|
||
const min = Math.min(...data);
|
||
const max = Math.max(...data);
|
||
const marge = Math.max((max - min) * 0.15, unite === '%' ? 2 : 0.2);
|
||
const yMin = min === max ? min - marge : min - marge;
|
||
const yMax = min === max ? max + marge : max + marge;
|
||
const range = yMax - yMin || 1;
|
||
|
||
// grille horizontale
|
||
ctx.lineWidth = 1;
|
||
for (let i = 0; i <= 4; i++) {
|
||
const y = pad.top + (h / 4) * i;
|
||
ctx.strokeStyle = '#0f3460';
|
||
ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(pad.left + w, y); ctx.stroke();
|
||
const val = yMax - (range / 4) * i;
|
||
ctx.fillStyle = '#a0aec0';
|
||
ctx.font = '10px system-ui';
|
||
ctx.textAlign = 'right';
|
||
ctx.textBaseline = 'middle';
|
||
ctx.fillText(val.toFixed(1) + unite, pad.left - 4, y);
|
||
}
|
||
|
||
if (data.length === 1) {
|
||
const x = pad.left + w / 2;
|
||
const y = pad.top + h - ((data[0] - yMin) / range) * h;
|
||
ctx.fillStyle = couleur;
|
||
ctx.beginPath();
|
||
ctx.arc(x, y, 4, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
ctx.fillStyle = '#a0aec0';
|
||
ctx.font = '11px system-ui';
|
||
ctx.textAlign = 'center';
|
||
ctx.fillText(data[0].toFixed(1) + unite, x, Math.max(12, y - 8));
|
||
return;
|
||
}
|
||
|
||
// courbe
|
||
ctx.strokeStyle = couleur;
|
||
ctx.lineWidth = 2;
|
||
ctx.lineJoin = 'round';
|
||
ctx.beginPath();
|
||
data.forEach((v, i) => {
|
||
const x = pad.left + (i / (data.length - 1)) * w;
|
||
const y = pad.top + h - ((v - yMin) / range) * h;
|
||
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
|
||
});
|
||
ctx.stroke();
|
||
|
||
// remplissage sous la courbe
|
||
ctx.lineTo(pad.left + w, pad.top + h);
|
||
ctx.lineTo(pad.left, pad.top + h);
|
||
ctx.closePath();
|
||
ctx.fillStyle = couleur + '22';
|
||
ctx.fill();
|
||
|
||
// labels temps (axe X — ~5 labels)
|
||
const now = Date.now();
|
||
const step = Math.max(1, Math.floor(data.length / 5));
|
||
ctx.fillStyle = '#a0aec0';
|
||
ctx.font = '10px system-ui';
|
||
ctx.textAlign = 'center';
|
||
ctx.textBaseline = 'alphabetic';
|
||
for (let i = 0; i < data.length; i += step) {
|
||
const x = pad.left + (i / (data.length - 1)) * w;
|
||
const t = new Date(now - (data.length - 1 - i) * stepSec * 1000);
|
||
ctx.fillText(
|
||
t.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }),
|
||
x, H - 5
|
||
);
|
||
}
|
||
}
|
||
|
||
// --- Long press relais dashboard ---
|
||
function getLongPressMs() {
|
||
return parseInt(localStorage.getItem('longpress_ms') || '500', 10);
|
||
}
|
||
|
||
function setupLongPress(carteId, relayNum) {
|
||
const el = document.getElementById(carteId);
|
||
if (!el) return;
|
||
let timer = null;
|
||
|
||
const start = (e) => {
|
||
e.preventDefault();
|
||
el.classList.add('press-hold');
|
||
timer = setTimeout(async () => {
|
||
timer = null;
|
||
el.classList.remove('press-hold');
|
||
el.classList.add('press-done');
|
||
setTimeout(() => el.classList.remove('press-done'), 500);
|
||
const res = await fetch('/api/relay/' + relayNum + '/toggle', { method: 'POST' });
|
||
if (!res.ok) {
|
||
const d = await res.json().catch(() => ({}));
|
||
afficherToast(d.err || 'Commande refusée');
|
||
} else {
|
||
afficherToast((relayNum === 1 ? noms.relay1 : noms.relay2) + ' basculé et sauvegardé');
|
||
}
|
||
rafraichir();
|
||
}, getLongPressMs());
|
||
};
|
||
|
||
const cancel = () => {
|
||
clearTimeout(timer);
|
||
timer = null;
|
||
el.classList.remove('press-hold');
|
||
};
|
||
|
||
el.addEventListener('pointerdown', start);
|
||
el.addEventListener('pointerup', cancel);
|
||
el.addEventListener('pointerleave', cancel);
|
||
el.addEventListener('contextmenu', e => e.preventDefault());
|
||
}
|
||
|
||
function setupSunLongPress() {
|
||
const el = document.getElementById('carte-sun');
|
||
if (!el) return;
|
||
let timer = null;
|
||
|
||
const start = (e) => {
|
||
e.preventDefault();
|
||
el.classList.add('press-hold');
|
||
timer = setTimeout(() => {
|
||
timer = null;
|
||
el.classList.remove('press-hold');
|
||
ouvrirSunPopup();
|
||
}, 2000);
|
||
};
|
||
|
||
const cancel = () => {
|
||
clearTimeout(timer);
|
||
timer = null;
|
||
el.classList.remove('press-hold');
|
||
};
|
||
|
||
el.addEventListener('pointerdown', start);
|
||
el.addEventListener('pointerup', cancel);
|
||
el.addEventListener('pointerleave', cancel);
|
||
el.addEventListener('contextmenu', e => e.preventDefault());
|
||
}
|
||
|
||
function formatSunHistoryTime(value) {
|
||
if (!value || value.indexOf('uptime') === 0) return value || '--';
|
||
const parts = value.split(' ');
|
||
if (parts.length !== 2) return value;
|
||
const d = parts[0].split('-');
|
||
const t = parts[1].split(':');
|
||
if (d.length !== 3 || t.length < 2) return value;
|
||
return t[0] + ':' + t[1] + ' ' + d[2] + '/' + d[1];
|
||
}
|
||
|
||
async function ouvrirSunPopup() {
|
||
const modal = document.getElementById('sun-modal');
|
||
const list = document.getElementById('sun-history-list');
|
||
if (!modal || !list) return;
|
||
|
||
modal.classList.remove('hidden');
|
||
list.textContent = 'Chargement...';
|
||
|
||
try {
|
||
const d = await (await fetch('/api/sun/history')).json();
|
||
const changes = d.changes || [];
|
||
if (!changes.length) {
|
||
list.innerHTML = '<div class="modal-empty">Aucun changement enregistré depuis le boot.</div>';
|
||
return;
|
||
}
|
||
list.innerHTML = changes.slice().reverse().map(c =>
|
||
'<div class="modal-row"><span>' + formatSunHistoryTime(c.time) + '</span><strong>' + c.label + '</strong></div>'
|
||
).join('');
|
||
} catch {
|
||
list.innerHTML = '<div class="modal-empty">Erreur lecture historique jour/nuit.</div>';
|
||
}
|
||
}
|
||
|
||
function fermerSunPopup() {
|
||
const modal = document.getElementById('sun-modal');
|
||
if (modal) modal.classList.add('hidden');
|
||
}
|
||
|
||
// --- MQTT ---
|
||
|
||
function mqttAfficherTopics(base) {
|
||
const info = document.getElementById('mqtt-topics-info');
|
||
const pub = document.getElementById('mqtt-topics-list');
|
||
const cmd = document.getElementById('mqtt-cmd-list');
|
||
if (!info || !pub || !cmd) return;
|
||
const b = base || 'solar';
|
||
const pubTopics = [
|
||
'pv/voltage', 'pv/current',
|
||
'battery/voltage', 'battery/soc', 'battery/temperature', 'battery/status',
|
||
'load/voltage', 'load/current', 'load/power',
|
||
'energy/generated/today', 'energy/generated/total',
|
||
'energy/consumed/today', 'energy/consumed/total',
|
||
'sun', 'rs485/ok', 'relay/1', 'relay/2', 'input/1', 'input/2'
|
||
];
|
||
pub.innerHTML = pubTopics.map(t => `<code>${b}/${t}</code>`).join(' ');
|
||
cmd.innerHTML = `<code>${b}/relay/1/set</code> <code>${b}/relay/2/set</code>` +
|
||
`<br><small style="color:var(--muted)">Valeurs acceptées : ON / OFF</small>`;
|
||
info.classList.remove('hidden');
|
||
}
|
||
|
||
async function chargerMqtt() {
|
||
try {
|
||
const d = await (await fetch('/api/mqtt')).json();
|
||
const bar = document.getElementById('mqtt-status-bar');
|
||
if (bar) {
|
||
if (d.enabled && d.connected) {
|
||
bar.textContent = '✓ Connecté — ' + d.server + ':' + d.port;
|
||
bar.className = 'ec-statusbar ec-ok';
|
||
} else if (d.enabled) {
|
||
bar.textContent = '⏳ Activé — en attente de connexion WiFi ou broker';
|
||
bar.className = 'ec-statusbar';
|
||
} else {
|
||
bar.textContent = 'Désactivé';
|
||
bar.className = 'ec-statusbar';
|
||
}
|
||
}
|
||
const s = id => document.getElementById(id);
|
||
s('mqtt-enabled').value = String(!!d.enabled);
|
||
if (s('mqtt-server')) s('mqtt-server').value = d.server || '192.168.1.36';
|
||
if (s('mqtt-port')) s('mqtt-port').value = d.port || 1883;
|
||
if (s('mqtt-user')) s('mqtt-user').value = d.user || '';
|
||
if (s('mqtt-pass')) s('mqtt-pass').value = d.pass || '';
|
||
if (s('mqtt-base')) s('mqtt-base').value = d.base || 'solar';
|
||
if (s('mqtt-interval')) s('mqtt-interval').value = d.interval || 30;
|
||
mqttAfficherTopics(d.base || 'solar');
|
||
} catch { /* silencieux */ }
|
||
}
|
||
|
||
async function sauvegarderMqtt() {
|
||
const g = id => (document.getElementById(id)?.value || '').trim();
|
||
const enabled = g('mqtt-enabled') === 'true';
|
||
const server = g('mqtt-server') || '192.168.1.36';
|
||
const port = parseInt(g('mqtt-port')) || 1883;
|
||
const user = g('mqtt-user');
|
||
const pass = g('mqtt-pass');
|
||
const base = g('mqtt-base') || 'solar';
|
||
const interval = parseInt(g('mqtt-interval')) || 30;
|
||
|
||
if (enabled && !server) { afficherToast('⚠ Adresse serveur requise'); return; }
|
||
|
||
try {
|
||
const res = await fetch('/api/mqtt', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ enabled, server, port, user, pass, base, interval })
|
||
});
|
||
const d = await res.json();
|
||
if (d.ok) {
|
||
afficherToast(enabled ? '✓ MQTT activé — connexion en cours' : '✓ MQTT désactivé');
|
||
mqttAfficherTopics(base);
|
||
setTimeout(chargerMqtt, 3000);
|
||
} else {
|
||
afficherToast('⚠ Erreur sauvegarde MQTT');
|
||
}
|
||
} catch(e) {
|
||
afficherToast('Erreur : ' + e.message);
|
||
}
|
||
}
|
||
|
||
// --- WireGuard ---
|
||
|
||
async function chargerWireGuard() {
|
||
try {
|
||
const d = await (await fetch('/api/wireguard')).json();
|
||
const bar = document.getElementById('wg-status-bar');
|
||
if (bar) {
|
||
if (d.enabled && d.connected) {
|
||
bar.textContent = '✓ Tunnel actif — ' + d.localip;
|
||
bar.className = 'ec-statusbar ec-ok';
|
||
} else if (d.enabled) {
|
||
bar.textContent = '⏳ Activé — en attente de connexion STA WiFi';
|
||
bar.className = 'ec-statusbar';
|
||
} else {
|
||
bar.textContent = 'Désactivé';
|
||
bar.className = 'ec-statusbar';
|
||
}
|
||
}
|
||
const s = id => document.getElementById(id);
|
||
s('wg-enabled').value = String(!!d.enabled);
|
||
if (s('wg-privkey')) s('wg-privkey').value = d.privkey || '';
|
||
if (s('wg-pubkey')) s('wg-pubkey').value = d.pubkey || '';
|
||
if (s('wg-psk')) s('wg-psk').value = d.psk || '';
|
||
if (s('wg-endpoint')) s('wg-endpoint').value = d.endpoint || '';
|
||
if (s('wg-port')) s('wg-port').value = d.port || 51820;
|
||
if (s('wg-localip')) s('wg-localip').value = d.localip || '';
|
||
if (s('wg-keepalive')) s('wg-keepalive').value = d.keepalive || 25;
|
||
} catch { /* silencieux */ }
|
||
}
|
||
|
||
async function sauvegarderWireGuard() {
|
||
const g = id => (document.getElementById(id)?.value || '').trim();
|
||
const enabled = g('wg-enabled') === 'true';
|
||
const privkey = g('wg-privkey');
|
||
const pubkey = g('wg-pubkey');
|
||
const psk = g('wg-psk');
|
||
const endpoint = g('wg-endpoint');
|
||
const port = parseInt(g('wg-port')) || 51820;
|
||
const localip = g('wg-localip');
|
||
const keepalive = parseInt(g('wg-keepalive')) || 25;
|
||
|
||
if (!privkey || !pubkey || !endpoint || !localip) {
|
||
afficherToast('⚠ Clé privée, clé publique, endpoint et IP locale sont obligatoires');
|
||
return;
|
||
}
|
||
if (privkey.length !== 44 || pubkey.length !== 44) {
|
||
afficherToast('⚠ Les clés WireGuard doivent faire 44 caractères base64');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const res = await fetch('/api/wireguard', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ enabled, privkey, pubkey, psk, endpoint, port, localip, keepalive })
|
||
});
|
||
const d = await res.json();
|
||
if (d.ok) {
|
||
afficherToast(enabled ? '✓ WireGuard activé — tunnel en cours d\'établissement' : '✓ WireGuard désactivé');
|
||
setTimeout(chargerWireGuard, 3000);
|
||
} else {
|
||
afficherToast('⚠ ' + (d.erreur || 'Erreur sauvegarde'));
|
||
}
|
||
} catch(e) {
|
||
afficherToast('Erreur : ' + e.message);
|
||
}
|
||
}
|
||
|
||
// --- Config EPEVER ---
|
||
|
||
const EC_KEYS = [
|
||
'battery_type', 'battery_capacity', 'temp_compensation',
|
||
'high_volt_disconnect', 'charging_limit', 'overvolt_reconnect',
|
||
'equalize_voltage', 'boost_voltage', 'float_voltage', 'boost_reconnect',
|
||
'low_reconnect', 'undervolt_warning', 'low_disconnect', 'discharge_limit',
|
||
'rated_voltage', 'equalize_duration', 'boost_duration', 'bat_discharge_soc'
|
||
];
|
||
|
||
function ecSetStatus(msg, ok) {
|
||
const el = document.getElementById('ec-status');
|
||
if (!el) return;
|
||
el.textContent = msg;
|
||
el.className = 'ec-statusbar ' + (ok === true ? 'ec-ok' : ok === false ? 'ec-err' : '');
|
||
}
|
||
|
||
function ecRemplirFormulaire(values) {
|
||
for (const key of EC_KEYS) {
|
||
const el = document.getElementById('ec-' + key);
|
||
if (!el || values[key] === undefined) continue;
|
||
el.value = values[key];
|
||
}
|
||
}
|
||
|
||
function ecLireFormulaire() {
|
||
const values = {};
|
||
for (const key of EC_KEYS) {
|
||
const el = document.getElementById('ec-' + key);
|
||
if (!el || el.value === '') continue;
|
||
values[key] = parseFloat(el.value);
|
||
}
|
||
return values;
|
||
}
|
||
|
||
async function lireConfigEpever() {
|
||
ecSetStatus('Lecture depuis l\'EPEVER en cours…');
|
||
try {
|
||
const res = await fetch('/api/epever/config');
|
||
const d = await res.json();
|
||
if (d.ok && d.values) {
|
||
ecRemplirFormulaire(d.values);
|
||
ecSetStatus('✓ Synchronisé — ' + new Date().toLocaleTimeString('fr-FR'), true);
|
||
} else {
|
||
ecSetStatus('⚠ ' + (d.erreur || 'Erreur RS485'), false);
|
||
}
|
||
} catch(e) {
|
||
ecSetStatus('✕ Erreur réseau : ' + e.message, false);
|
||
}
|
||
}
|
||
|
||
async function ecrireConfigEpever() {
|
||
if (!confirm(
|
||
'Confirmer l\'écriture des paramètres vers le régulateur EPEVER ?\n\n' +
|
||
'Une valeur incorrecte peut endommager la batterie.\n\n' +
|
||
'Vérifier les valeurs avant de confirmer.'
|
||
)) return;
|
||
|
||
ecSetStatus('Écriture en cours…');
|
||
const values = ecLireFormulaire();
|
||
try {
|
||
const res = await fetch('/api/epever/config', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(values)
|
||
});
|
||
const d = await res.json();
|
||
if (d.ok) {
|
||
afficherToast('✓ Paramètres écrits dans l\'EPEVER');
|
||
ecSetStatus('✓ Écrit — ' + new Date().toLocaleTimeString('fr-FR'), true);
|
||
} else {
|
||
const msg = d.erreur || 'Erreur écriture';
|
||
afficherToast('⚠ ' + msg);
|
||
ecSetStatus('⚠ ' + msg, false);
|
||
}
|
||
} catch(e) {
|
||
ecSetStatus('✕ Erreur réseau : ' + e.message, false);
|
||
}
|
||
}
|
||
|
||
async function sauvegarderConfigEpever() {
|
||
try {
|
||
const res = await fetch('/api/epever/config/save', { method: 'POST' });
|
||
const d = await res.json();
|
||
afficherToast(d.ok ? '✓ Config sauvegardée dans LittleFS' : ('⚠ ' + (d.erreur || 'Erreur sauvegarde')));
|
||
} catch(e) {
|
||
afficherToast('✕ Erreur : ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function restaurerConfigEpever() {
|
||
if (!confirm('Restaurer la configuration sauvegardée et l\'écrire dans l\'EPEVER ?')) return;
|
||
ecSetStatus('Restauration en cours…');
|
||
try {
|
||
const res = await fetch('/api/epever/config/restore', { method: 'POST' });
|
||
const d = await res.json();
|
||
if (d.ok) {
|
||
afficherToast('✓ Config restaurée');
|
||
await lireConfigEpever();
|
||
} else {
|
||
const msg = d.erreur || 'Erreur restauration';
|
||
afficherToast('⚠ ' + msg);
|
||
ecSetStatus('⚠ ' + msg, false);
|
||
}
|
||
} catch(e) {
|
||
ecSetStatus('✕ Erreur : ' + e.message, false);
|
||
}
|
||
}
|
||
|
||
async function exporterConfigEpever() {
|
||
try {
|
||
const res = await fetch('/api/epever/config/saved');
|
||
if (!res.ok) { afficherToast('Aucune config sauvegardée'); return; }
|
||
const text = await res.text();
|
||
const blob = new Blob([text], { type: 'application/json' });
|
||
const a = document.createElement('a');
|
||
a.href = URL.createObjectURL(blob);
|
||
a.download = 'epever_config.json';
|
||
a.click();
|
||
URL.revokeObjectURL(a.href);
|
||
} catch(e) {
|
||
afficherToast('Erreur export : ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function importerConfigEpever(input) {
|
||
const file = input.files[0];
|
||
if (!file) return;
|
||
try {
|
||
const text = await file.text();
|
||
const obj = JSON.parse(text);
|
||
const vals = obj.values || obj;
|
||
ecRemplirFormulaire(vals);
|
||
afficherToast('Config importée — vérifier puis cliquer Écrire pour appliquer');
|
||
ecSetStatus('Fichier importé — non encore écrit dans l\'EPEVER');
|
||
} catch(e) {
|
||
afficherToast('Erreur import JSON : ' + e.message);
|
||
}
|
||
input.value = '';
|
||
}
|
||
|
||
// --- Démarrage ---
|
||
chargerNoms();
|
||
rafraichir();
|
||
setInterval(rafraichir, REFRESH_MS);
|
||
setInterval(() => {
|
||
const debugEl = document.getElementById('debug');
|
||
const debugActif = debugEl && debugEl.classList.contains('actif');
|
||
if (debugActif) chargerDebug();
|
||
}, 2000);
|
||
setInterval(() => {
|
||
const histEl = document.getElementById('historique');
|
||
const histActif = histEl && histEl.classList.contains('actif');
|
||
if (histActif) chargerHistorique();
|
||
}, 5000);
|
||
setupLongPress('carte-relay1', 1);
|
||
setupLongPress('carte-relay2', 2);
|
||
setupSunLongPress();
|