Files
kc868-a2_solar/emulator/server.py
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

112 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""
Serveur HTTP pour l'interface de débogage 3 volets.
Port 8888 :
GET / → page HTML 3 volets
GET /serial → SSE (Server-Sent Events) du port série QEMU
GET /api/* → proxy transparent vers le webserver ESP32 (port 10080)
POST /api/* → idem
"""
import http.server
import socketserver
import time
import urllib.request
import urllib.error
from urllib.parse import urlparse
ESP_URL = 'http://127.0.0.1:10080'
SERIAL_LOG = '/tmp/serial.log'
PORT = 8888
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
if self.path in ('/', '/index.html'):
self._serve_file('/emulator/ui/index.html', 'text/html; charset=utf-8')
elif self.path == '/serial':
self._sse_serial()
elif self.path.startswith('/api/'):
self._proxy('GET')
else:
self.send_error(404)
def do_POST(self):
if self.path.startswith('/api/'):
self._proxy('POST')
else:
self.send_error(404)
# ------------------------------------------------------------------
def _serve_file(self, path, content_type):
try:
with open(path, 'rb') as f:
data = f.read()
self.send_response(200)
self.send_header('Content-Type', content_type)
self.send_header('Content-Length', str(len(data)))
self.end_headers()
self.wfile.write(data)
except FileNotFoundError:
self.send_error(404)
def _sse_serial(self):
self.send_response(200)
self.send_header('Content-Type', 'text/event-stream')
self.send_header('Cache-Control', 'no-cache')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
try:
# Ouvrir le fichier log et se positionner à la fin
with open(SERIAL_LOG, 'r', errors='replace') as f:
f.seek(0, 2)
while True:
line = f.readline()
if line:
msg = line.rstrip().replace('\n', ' ')
self.wfile.write(f'data: {msg}\n\n'.encode())
self.wfile.flush()
else:
time.sleep(0.1)
except (BrokenPipeError, ConnectionResetError):
pass
except FileNotFoundError:
# Log pas encore créé — attendre
time.sleep(1)
def _proxy(self, method):
target = ESP_URL + self.path
try:
length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(length) if length else None
req = urllib.request.Request(target, data=body, method=method)
if body:
req.add_header('Content-Type', self.headers.get('Content-Type', 'application/json'))
with urllib.request.urlopen(req, timeout=3) as resp:
data = resp.read()
self.send_response(resp.status)
self.send_header('Content-Type', resp.headers.get('Content-Type', 'application/json'))
self.send_header('Content-Length', str(len(data)))
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(data)
except urllib.error.URLError:
self.send_response(503)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(b'{"error":"ESP32 hors ligne"}')
def log_message(self, *_):
pass # supprimer les logs d'accès
if __name__ == '__main__':
with socketserver.ThreadingTCPServer(('', PORT), Handler) as httpd:
httpd.allow_reuse_address = True
print(f'[UI] Interface de débogage sur http://0.0.0.0:{PORT}', flush=True)
httpd.serve_forever()