Files
kc868-a2_solar/backup/filesystem/app.js
T
gilles a8f0d6ccba Initial commit — KC868-A2 contrôleur solaire ESP32
Fonctionnalités :
- Lecture RS485 Modbus Epever Tracer 4210N (115200 bps, FC03/FC04/FC16)
- Moteur de règles JSON (LittleFS) — commande automatique des relais
- Interface web mobile-first (dashboard, règles, config, historique, EPEVER, debug)
- WiFi AP+STA simultanés avec reconnexion automatique et portail captif
- mDNS configurable (pv.local par défaut)
- Configuration registres EPEVER depuis l'UI (18 registres holding)
- Historique basse/haute résolution avec graphes canvas
- VPN WireGuard optionnel (désactivé par défaut, config via UI)
- OTA firmware + filesystem via ElegantOTA
- Deep sleep / économie d'énergie

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 19:25:01 +02:00

707 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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(); }
if (nom === 'historique') chargerHistorique();
if (nom === 'debug') chargerDebug();
}
// --- 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 info ---
async function chargerWifi() {
try {
const d = await (await fetch('/api/wifi')).json();
setText('wifi-ssid', d.ssid || '--');
setText('wifi-pwd', d.password || '(vide)');
} catch { /* silencieux */ }
}
// --- 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 lAPI.';
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');
}
// --- 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();