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>
This commit is contained in:
2026-05-09 19:25:01 +02:00
commit a8f0d6ccba
88 changed files with 13162 additions and 0 deletions
+332
View File
@@ -0,0 +1,332 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>KC868-A2 — Émulateur QEMU</title>
<style>
:root {
--bg: #0d1117;
--surface: #161b22;
--border: #30363d;
--accent: #e94560;
--vert: #00b894;
--rouge: #d63031;
--jaune: #fdcb6e;
--bleu: #74b9ff;
--texte: #e6edf3;
--muted: #8b949e;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
color: var(--texte);
font-family: 'Segoe UI', system-ui, sans-serif;
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
/* === En-tête === */
header {
background: var(--surface);
border-bottom: 1px solid var(--border);
padding: 0.5rem 1rem;
display: flex;
align-items: center;
gap: 1rem;
font-size: 0.85rem;
flex-shrink: 0;
}
header h1 { font-size: 0.95rem; font-weight: 600; }
.badge {
padding: 0.15rem 0.6rem;
border-radius: 999px;
font-size: 0.7rem;
font-weight: 700;
}
.badge-ok { background: var(--vert); color: #000; }
.badge-err { background: var(--rouge); color: #fff; }
.badge-warn{ background: var(--jaune); color: #000; }
.spacer { flex: 1; }
/* === Layout 3 volets === */
.layout {
display: flex;
flex: 1;
overflow: hidden;
}
/* Volet gauche — état GPIO */
.panel-gpio {
width: 220px;
flex-shrink: 0;
background: var(--surface);
border-right: 1px solid var(--border);
overflow-y: auto;
padding: 0.75rem;
}
.section {
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
margin: 0.75rem 0 0.4rem;
}
.section:first-child { margin-top: 0; }
.gpio-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.35rem;
font-size: 0.8rem;
}
.gpio-label { color: var(--muted); }
.gpio-val { font-weight: 600; font-family: monospace; }
.on { color: var(--vert); }
.off { color: var(--muted); }
.err { color: var(--rouge); }
.num { color: var(--bleu); }
/* Volet droit — webserver + serial */
.panel-right {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
iframe {
flex: 1;
border: none;
background: #1a1a2e;
}
/* Terminal série */
.serial-bar {
height: 36px;
background: var(--surface);
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
padding: 0 0.75rem;
font-size: 0.7rem;
color: var(--muted);
flex-shrink: 0;
gap: 0.5rem;
cursor: pointer;
user-select: none;
}
.serial-bar:hover { color: var(--texte); }
.terminal {
height: 180px;
background: #0a0c10;
overflow-y: auto;
padding: 0.4rem 0.75rem;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 0.72rem;
flex-shrink: 0;
transition: height 0.2s;
}
.terminal.collapsed { height: 0; padding: 0; }
.log-line { color: #b2bec3; white-space: pre-wrap; line-height: 1.45; }
.log-line.warn { color: var(--jaune); }
.log-line.err { color: var(--rouge); }
.log-line.ok { color: var(--vert); }
</style>
</head>
<body>
<header>
<h1>⚡ KC868-A2 — Émulateur QEMU ESP32</h1>
<span id="badge-esp" class="badge badge-err">ESP32 démarrage…</span>
<span id="badge-mb" class="badge badge-err">Modbus --</span>
<span class="spacer"></span>
<span style="color:var(--muted);font-size:0.7rem">
WebServer : <a href="http://localhost:10080" target="_blank"
style="color:var(--bleu)">localhost:10080</a>
</span>
</header>
<div class="layout">
<!-- Volet gauche : état système -->
<div class="panel-gpio">
<div class="section">RS485 / Modbus</div>
<div class="gpio-row">
<span class="gpio-label">État</span>
<span class="gpio-val" id="g-rs485">--</span>
</div>
<div class="section">Relais</div>
<div class="gpio-row">
<span class="gpio-label">GPIO15 Relay 1</span>
<span class="gpio-val" id="g-r1">--</span>
</div>
<div class="gpio-row">
<span class="gpio-label">GPIO2 Relay 2</span>
<span class="gpio-val" id="g-r2">--</span>
</div>
<div class="section">Entrées numériques</div>
<div class="gpio-row">
<span class="gpio-label">GPIO36 DI1</span>
<span class="gpio-val" id="g-di1">--</span>
</div>
<div class="gpio-row">
<span class="gpio-label">GPIO39 DI2</span>
<span class="gpio-val" id="g-di2">--</span>
</div>
<div class="section">Mode</div>
<div class="gpio-row">
<span class="gpio-label">Contrôle</span>
<span class="gpio-val" id="g-mode">--</span>
</div>
<div class="section">Solaire</div>
<div class="gpio-row">
<span class="gpio-label">Ensoleillement</span>
<span class="gpio-val" id="g-sun">--</span>
</div>
<div class="gpio-row">
<span class="gpio-label">Batterie</span>
<span class="gpio-val num" id="g-bat">--</span>
</div>
<div class="gpio-row">
<span class="gpio-label">Tension PV</span>
<span class="gpio-val num" id="g-pv">--</span>
</div>
<div class="gpio-row">
<span class="gpio-label">Courant PV</span>
<span class="gpio-val num" id="g-pvc">--</span>
</div>
<div class="gpio-row">
<span class="gpio-label">SOC</span>
<span class="gpio-val num" id="g-soc">--</span>
</div>
<div class="gpio-row">
<span class="gpio-label">Temp. bat.</span>
<span class="gpio-val num" id="g-temp">--</span>
</div>
<div class="gpio-row">
<span class="gpio-label">Statut charge</span>
<span class="gpio-val" id="g-stat">--</span>
</div>
<div class="section">Load</div>
<div class="gpio-row">
<span class="gpio-label">Puissance</span>
<span class="gpio-val num" id="g-load">--</span>
</div>
<div class="section">Énergie</div>
<div class="gpio-row">
<span class="gpio-label">Prod. jour</span>
<span class="gpio-val num" id="g-egenj">--</span>
</div>
<div class="gpio-row">
<span class="gpio-label">Conso. jour</span>
<span class="gpio-val num" id="g-econj">--</span>
</div>
</div>
<!-- Volet droit : iframe webserver + terminal série -->
<div class="panel-right">
<iframe id="esp-frame" src="http://localhost:10080" title="WebServer ESP32"></iframe>
<div class="serial-bar" onclick="toggleTerminal()">
<span>▼ Terminal série (UART0)</span>
<span id="serial-count" style="margin-left:auto;font-family:monospace">0 lignes</span>
</div>
<div class="terminal" id="terminal"></div>
</div>
</div>
<script>
'use strict';
// --- Polling état ESP32 ---
async function pollState() {
try {
const d = await (await fetch('/api/state', { signal: AbortSignal.timeout(2000) })).json();
badge('badge-esp', d.rs485_ok ? 'ESP32 OK' : 'ESP32 WiFi', d.rs485_ok ? 'ok' : 'warn');
badge('badge-mb', d.rs485_ok ? 'Modbus OK' : 'Modbus ERR', d.rs485_ok ? 'ok' : 'err');
gpio('g-rs485', d.rs485_ok ? '● OK' : '○ ERR', d.rs485_ok ? 'on' : 'err');
gpio('g-r1', d.relay1 ? '● ON' : '○ OFF', d.relay1 ? 'on' : 'off');
gpio('g-r2', d.relay2 ? '● ON' : '○ OFF', d.relay2 ? 'on' : 'off');
gpio('g-di1', d.di1 ? '● APP' : '○ REL', d.di1 ? 'on' : 'off');
gpio('g-di2', d.di2 ? '● APP' : '○ REL', d.di2 ? 'on' : 'off');
gpio('g-mode', d.autoMode ? 'Auto' : 'Manuel', 'num');
gpio('g-sun', d.sun ? '☀ Jour' : '🌙 Nuit', d.sun ? 'on' : 'off');
num('g-bat', d.battery.toFixed(2) + ' V');
num('g-pv', d.pv.toFixed(2) + ' V');
num('g-pvc', d.pvCurrent.toFixed(2) + ' A');
num('g-soc', d.batSOC + ' %');
num('g-temp', d.batTemperature.toFixed(1) + ' °C');
num('g-load', d.loadPower.toFixed(1) + ' W');
num('g-egenj',d.energieGenJour.toFixed(2) + ' kWh');
num('g-econj',d.energieConJour.toFixed(2) + ' kWh');
const statuts = ['Arrêt', 'Float', 'Boost', 'Égalisation'];
gpio('g-stat', statuts[d.batStatut] || '--', 'num');
} catch {
badge('badge-esp', 'ESP32 hors ligne', 'err');
gpio('g-rs485', '○ ERR', 'err');
}
}
function badge(id, txt, type) {
const el = document.getElementById(id);
el.textContent = txt;
el.className = 'badge badge-' + type;
}
function gpio(id, txt, cls) {
const el = document.getElementById(id);
el.textContent = txt;
el.className = 'gpio-val ' + cls;
}
function num(id, txt) {
const el = document.getElementById(id);
el.textContent = txt;
el.className = 'gpio-val num';
}
// --- Terminal série via SSE ---
let lineCount = 0;
const terminal = document.getElementById('terminal');
const evtSrc = new EventSource('/serial');
evtSrc.onmessage = e => {
const line = document.createElement('div');
line.className = 'log-line'
+ (e.data.match(/err|error|erreur|fail/i) ? ' err' : '')
+ (e.data.match(/warn|timeout|hors ligne/i) ? ' warn' : '')
+ (e.data.match(/ok|prêt|ready|démarr/i) ? ' ok' : '');
line.textContent = e.data;
terminal.appendChild(line);
lineCount++;
document.getElementById('serial-count').textContent = lineCount + ' lignes';
// Garder max 1000 lignes
while (terminal.children.length > 1000) terminal.removeChild(terminal.firstChild);
terminal.scrollTop = terminal.scrollHeight;
};
// --- Toggle terminal ---
function toggleTerminal() {
terminal.classList.toggle('collapsed');
}
// --- Démarrage ---
pollState();
setInterval(pollState, 3000);
</script>
</body>
</html>