Files
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

333 lines
11 KiB
HTML
Raw Permalink 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.
<!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>