Files
cariflex/scripts/simulation_dashboard.py
2026-06-09 18:59:37 -04:00

300 lines
12 KiB
Python

#!/usr/bin/env python3
"""Cariflex - Script optimal de simulation avec page web intégrée."""
import json
import requests
import re
import warnings
import random
import math
from datetime import datetime, timezone, timedelta
from flask import Flask, render_template_string, request, jsonify
import pytz
warnings.filterwarnings("ignore")
FM_HOST = "https://cariflex.digitribe.fr"
CREDS_FILE = "/tmp/fm_creds.json"
MARTINIQUE_TZ = pytz.timezone("America/Martinique")
# ========================================
# Web App
# ========================================
app = Flask(__name__)
DASHBOARD_HTML = """
<!DOCTYPE html>
<html>
<head>
<title>Cariflex EMS - Simulation</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body { background: #1a1a2e; color: #eee; }
.card { background: #16213e; border: 1px solid #0f3460; }
.card-header { background: #0f3460; }
.stat-value { font-size: 2rem; font-weight: bold; color: #e94560; }
.stat-label { font-size: 0.8rem; color: #888; }
.refresh-btn { background: #e94560; border: none; }
.refresh-btn:hover { background: #c73e54; }
#log { background: #0d1117; color: #0f0; font-family: monospace; max-height: 300px; overflow-y: auto; padding: 10px; font-size: 0.8rem; }
</style>
</head>
<body class="p-4">
<div class="container-fluid">
<h1 class="mb-4">🌴 Cariflex EMS - Simulation Temps Réel</h1>
<div class="row mb-4">
<div class="col-md-3">
<div class="card text-center p-3">
<div class="stat-value" id="pv_power">--</div>
<div class="stat-label">Production PV (kW)</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center p-3">
<div class="stat-value" id="bat_soc">--</div>
<div class="stat-label">SOC Batteries (kWh)</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center p-3">
<div class="stat-value" id="ev_load">--</div>
<div class="stat-label">Charge VE (kW)</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center p-3">
<div class="stat-value" id="price">--</div>
<div class="stat-label">Prix DSO (EUR/MWh)</div>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-md-3">
<div class="card text-center p-3">
<div class="stat-value" id="irradiance">--</div>
<div class="stat-label">Irradiance (W/m²)</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center p-3">
<div class="stat-value" id="temperature">--</div>
<div class="stat-label">Température (°C)</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center p-3">
<div class="stat-value" id="flexibility">--</div>
<div class="stat-label">Flexibilité (kW)</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center p-3">
<div class="stat-value" id="balance">--</div>
<div class="stat-label">Balance Réseau (kW)</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>📊 Données en temps réel</span>
<button class="btn btn-sm refresh-btn" onclick="refreshData()">🔄 Rafraîchir</button>
</div>
<div class="card-body">
<div id="log">Chargement...</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">⚙️ Paramètres</div>
<div class="card-body">
<form id="paramsForm">
<div class="mb-3">
<label class="form-label">Intervalle simulation (sec)</label>
<input type="number" class="form-control" id="interval" value="30" min="5" max="300">
</div>
<div class="mb-3">
<label class="form-label">Multiplicateur PV</label>
<input type="range" class="form-range" id="pv_mult" min="0.5" max="2" step="0.1" value="1">
</div>
<div class="mb-3">
<label class="form-label">Multiplicateur Charge VE</label>
<input type="range" class="form-range" id="ev_mult" min="0.5" max="2" step="0.1" value="1">
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="use_real_weather" checked>
<label class="form-check-label">Données météo réelles</label>
</div>
<button type="submit" class="btn btn-primary w-100">Appliquer</button>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
function refreshData() {
fetch('/api/data')
.then(r => r.json())
.then(data => {
document.getElementById('pv_power').textContent = data.pv_power?.toFixed(1) || '--';
document.getElementById('bat_soc').textContent = data.bat_soc?.toFixed(1) || '--';
document.getElementById('ev_load').textContent = data.ev_load?.toFixed(1) || '--';
document.getElementById('price').textContent = data.price?.toFixed(2) || '--';
document.getElementById('irradiance').textContent = data.irradiance?.toFixed(0) || '--';
document.getElementById('temperature').textContent = data.temperature?.toFixed(1) || '--';
document.getElementById('flexibility').textContent = data.flexibility?.toFixed(1) || '--';
document.getElementById('balance').textContent = data.balance?.toFixed(1) || '--';
document.getElementById('log').innerHTML = data.log || 'Pas de données';
});
}
document.getElementById('paramsForm').onsubmit = function(e) {
e.preventDefault();
const params = {
interval: document.getElementById('interval').value,
pv_mult: document.getElementById('pv_mult').value,
ev_mult: document.getElementById('ev_mult').value,
use_real_weather: document.getElementById('use_real_weather').checked
};
fetch('/api/params', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(params)})
.then(r => r.json())
.then(data => { alert('Paramètres appliqués'); refreshData(); });
};
setInterval(refreshData, 5000);
refreshData();
</script>
</body>
</html>
"""
# Global state
sim_state = {
"interval": 30,
"pv_mult": 1.0,
"ev_mult": 1.0,
"use_real_weather": True,
"running": False,
"log": [],
"last_data": {}
}
def fm_login():
session = requests.Session()
session.verify = False
r = session.get(f"{FM_HOST}/login")
match = re.search(r'<input[^>]*csrf_token[^>]*value="([^"]+)"', r.text)
csrf = match.group(1)
r = session.post(f"{FM_HOST}/login", data={
"email": "admin@digitribe.fr", "password": "Digitribe972",
"csrf_token": csrf, "remember": "y"
}, allow_redirects=True)
return session if "dashboard" in r.url else None
def get_fm_data(session):
"""Get latest data from FM sensors."""
data = {}
# Get sensor data via API
sensors = {
"pv": (41, 50), # PV sensors
"bat": (51, 60), # Battery sensors
"ev": (61, 70), # EV charger sensors
"v2g": (71, 80), # V2G sensors
"irradiance": 81,
"temperature": 82,
"consumption_price": 84,
}
for name, sensor_range in sensors.items():
if isinstance(sensor_range, tuple):
# Multiple sensors - get aggregate
total = 0
count = 0
for sid in range(sensor_range[0], sensor_range[1] + 1):
r = session.get(f"{FM_HOST}/api/v3_0/sensors/{sid}/data?limit=1")
if r.status_code == 200:
try:
vals = r.json()
if vals and len(vals) > 0:
total += vals[-1].get("event_value", 0)
count += 1
except:
pass
data[name] = total if count > 0 else 0
else:
# Single sensor
r = session.get(f"{FM_HOST}/api/v3_0/sensors/{sensor_range}/data?limit=1")
if r.status_code == 200:
try:
vals = r.json()
if vals and len(vals) > 0:
data[name] = vals[-1].get("event_value", 0)
except:
pass
return data
@app.route("/")
def index():
return render_template_string(DASHBOARD_HTML)
@app.route("/api/data")
def api_data():
session = fm_login()
if not session:
return jsonify({"error": "Auth failed"})
data = get_fm_data(session)
# Calculate derived values
pv_power = data.get("pv", 0)
bat_soc = data.get("bat", 0)
ev_load = data.get("ev", 0)
v2g_power = data.get("v2g", 0)
price = data.get("consumption_price", 0)
irradiance = data.get("irradiance", 0)
temperature = data.get("temperature", 0)
# Flexibility = total battery + V2G capacity
flexibility = bat_soc + v2g_power
# Balance = Production - Consumption
balance = pv_power - ev_load
result = {
"pv_power": pv_power,
"bat_soc": bat_soc,
"ev_load": ev_load,
"price": price,
"irradiance": irradiance,
"temperature": temperature,
"flexibility": flexibility,
"balance": balance,
"log": "<br>".join(sim_state["log"][-20:]) if sim_state["log"] else "En attente de données..."
}
sim_state["last_data"] = result
return jsonify(result)
@app.route("/api/params", methods=["POST"])
def api_params():
params = request.json
sim_state["interval"] = int(params.get("interval", 30))
sim_state["pv_mult"] = float(params.get("pv_mult", 1.0))
sim_state["ev_mult"] = float(params.get("ev_mult", 1.0))
sim_state["use_real_weather"] = bool(params.get("use_real_weather", True))
return jsonify({"status": "ok"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5001, debug=False)