Grafana dashboard flexibilite/agregation
This commit is contained in:
299
scripts/simulation_dashboard.py
Normal file
299
scripts/simulation_dashboard.py
Normal file
@@ -0,0 +1,299 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user