Grafana dashboard flexibilite/agregation
This commit is contained in:
78
scripts/create_sim_assets.py
Normal file
78
scripts/create_sim_assets.py
Normal file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Cariflex - Crée les sensors météo et prix DSO pour la simulation."""
|
||||
|
||||
import json
|
||||
import requests
|
||||
import re
|
||||
import warnings
|
||||
warnings.filterwarnings("ignore")
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
FM_HOST = "https://cariflex.digitribe.fr"
|
||||
CREDS_FILE = "/tmp/fm_creds.json"
|
||||
|
||||
with open(CREDS_FILE) as f:
|
||||
creds = json.load(f)
|
||||
|
||||
session = requests.Session()
|
||||
session.verify = False
|
||||
|
||||
# Login
|
||||
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": creds["email"], "password": creds["password"],
|
||||
"csrf_token": csrf, "remember": "y"
|
||||
}, allow_redirects=True)
|
||||
print(f"Login: {'OK' if 'dashboard' in r.url else 'FAILED'}")
|
||||
|
||||
# Step 1: Check existing asset types
|
||||
r = session.get(f"{FM_HOST}/api/v3_0/generic_asset_types")
|
||||
print(f"Asset types: HTTP {r.status_code}")
|
||||
types = {}
|
||||
if r.status_code == 200:
|
||||
for t in r.json():
|
||||
types[t["name"]] = t["id"]
|
||||
print(f" {t['id']}: {t['name']}")
|
||||
|
||||
# Step 2: Check existing assets
|
||||
r = session.get(f"{FM_HOST}/api/v3_0/assets")
|
||||
print(f"\nAssets: HTTP {r.status_code}")
|
||||
assets = {}
|
||||
if r.status_code == 200:
|
||||
for a in r.json()[:10]:
|
||||
assets[a["name"]] = a["id"]
|
||||
print(f" {a['id']}: {a['name']} ({a.get('generic_asset_type', {}).get('name', '?')})")
|
||||
|
||||
# Step 3: Create assets for weather and pricing
|
||||
# Use existing asset types or create generic ones
|
||||
print("\n=== Création des assets ===")
|
||||
|
||||
new_assets = [
|
||||
{"name": "Météo Martinique", "type_id": 1, "lat": 14.6091, "lon": -61.2155},
|
||||
{"name": "Marché Énergie", "type_id": 1, "lat": 14.6091, "lon": -61.2155},
|
||||
]
|
||||
|
||||
for asset in new_assets:
|
||||
r = session.post(f"{FM_HOST}/api/v3_0/assets", json={
|
||||
"name": asset["name"],
|
||||
"generic_asset_type_id": asset["type_id"],
|
||||
"latitude": asset["lat"],
|
||||
"longitude": asset["lon"],
|
||||
})
|
||||
print(f" Asset '{asset['name']}': HTTP {r.status_code}")
|
||||
if r.status_code == 201:
|
||||
aid = r.json().get("id")
|
||||
print(f" ID: {aid}")
|
||||
assets[asset["name"]] = aid
|
||||
else:
|
||||
try:
|
||||
err = r.json()
|
||||
print(f" Error: {err.get('message', r.text[:200])}")
|
||||
except:
|
||||
print(f" Error: {r.text[:200]}")
|
||||
|
||||
print(f"\n=== Assets existants ===")
|
||||
for name, aid in assets.items():
|
||||
print(f" {aid}: {name}")
|
||||
92
scripts/create_simulation_assets.py
Normal file
92
scripts/create_simulation_assets.py
Normal file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Cariflex - Crée les assets et sensors pour la simulation météo et prix DSO."""
|
||||
|
||||
import json
|
||||
import requests
|
||||
import re
|
||||
import warnings
|
||||
warnings.filterwarnings("ignore")
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
FM_HOST = "https://cariflex.digitribe.fr"
|
||||
CREDS_FILE = "/tmp/fm_creds.json"
|
||||
|
||||
with open(CREDS_FILE) as f:
|
||||
creds = json.load(f)
|
||||
|
||||
session = requests.Session()
|
||||
session.verify = False
|
||||
|
||||
# Login
|
||||
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": creds["email"], "password": creds["password"],
|
||||
"csrf_token": csrf, "remember": "y"
|
||||
}, allow_redirects=True)
|
||||
print(f"Login: {'OK' if 'dashboard' in r.url else 'FAILED'}")
|
||||
|
||||
# Get auth token for API
|
||||
token = r.cookies.get("session", "")
|
||||
headers = {"Authorization": f"Bearer {token}"} if token else {}
|
||||
|
||||
def fm_post(path, data=None, json_data=None):
|
||||
url = f"{FM_HOST}/api/v3_0{path}"
|
||||
r = session.post(url, data=data, json=json_data)
|
||||
return r
|
||||
|
||||
def fm_get(path):
|
||||
url = f"{FM_HOST}/api/v3_0{path}"
|
||||
r = session.get(url)
|
||||
return r
|
||||
|
||||
# ========================================
|
||||
# 1. Créer les assets météo
|
||||
# ========================================
|
||||
print("\n=== Création des assets météo ===")
|
||||
|
||||
meteo_assets = [
|
||||
{
|
||||
"name": "Météo Martinique",
|
||||
"asset_type_name": "Weather Station",
|
||||
"latitude": 14.6091,
|
||||
"longitude": -61.2155,
|
||||
"sensors": [
|
||||
{"name": "irradiance", "unit": "W/m²"},
|
||||
{"name": "temperature", "unit": "°C"},
|
||||
{"name": "wind_speed", "unit": "m/s"},
|
||||
{"name": "humidity", "unit": "%"},
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Prix DSO",
|
||||
"asset_type_name": "Market",
|
||||
"latitude": 14.6091,
|
||||
"longitude": -61.2155,
|
||||
"sensors": [
|
||||
{"name": "consumption_price", "unit": "EUR/MWh"},
|
||||
{"name": "production_price", "unit": "EUR/MWh"},
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
for asset in meteo_assets:
|
||||
r = fm_post("/assets", json_data={
|
||||
"name": asset["name"],
|
||||
"latitude": asset["latitude"],
|
||||
"longitude": asset["longitude"],
|
||||
})
|
||||
print(f" Asset '{asset['name']}': HTTP {r.status_code}")
|
||||
|
||||
if r.status_code in [200, 201]:
|
||||
try:
|
||||
resp = r.json()
|
||||
asset_id = resp.get("id")
|
||||
print(f" ID: {asset_id}")
|
||||
except:
|
||||
print(f" Response: {r.text[:200]}")
|
||||
else:
|
||||
print(f" Error: {r.text[:200]}")
|
||||
|
||||
print("\n=== Simulation setup complete ===")
|
||||
132
scripts/inject_final.py
Normal file
132
scripts/inject_final.py
Normal file
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Cariflex - Injecte les données météo réelles et prix DSO dans FM via le CLI."""
|
||||
|
||||
import subprocess
|
||||
import json
|
||||
import random
|
||||
import math
|
||||
from datetime import datetime, timezone, timedelta
|
||||
import pytz
|
||||
import requests
|
||||
|
||||
MARTINIQUE_TZ = pytz.timezone("America/Martinique")
|
||||
|
||||
# Sensor IDs (from previous creation)
|
||||
SENSORS = {
|
||||
"irradiance": 81,
|
||||
"temperature": 82,
|
||||
"wind_speed": 83,
|
||||
"consumption_price": 84,
|
||||
"production_price": 85,
|
||||
}
|
||||
|
||||
def run_fm_cli(args):
|
||||
"""Run FM CLI inside the container."""
|
||||
cmd = ["docker", "exec", "flexmeasures-server", "bash", "-c",
|
||||
f"cd /app && .venv/bin/flexmeasures {' '.join(args)}"]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
||||
return result.stdout.strip(), result.stderr.strip(), result.returncode
|
||||
|
||||
def post_sensor_data(sensor_id, values, unit, start, duration="PT1H"):
|
||||
"""Post sensor data via FM API."""
|
||||
import re
|
||||
|
||||
# Login first
|
||||
session = requests.Session()
|
||||
session.verify = False
|
||||
r = session.get("https://cariflex.digitribe.fr/login")
|
||||
match = re.search(r'<input[^>]*csrf_token[^>]*value="([^"]+)"', r.text)
|
||||
if not match:
|
||||
return False
|
||||
csrf = match.group(1)
|
||||
r = session.post("https://cariflex.digitribe.fr/login", data={
|
||||
"email": "admin@digitribe.fr", "password": "Digitribe972",
|
||||
"csrf_token": csrf, "remember": "y"
|
||||
}, allow_redirects=True)
|
||||
|
||||
if "dashboard" not in r.url:
|
||||
return False
|
||||
|
||||
# Post data - use proper ISO format with timezone
|
||||
r = session.post(
|
||||
f"https://cariflex.digitribe.fr/api/v3_0/sensors/{sensor_id}/data",
|
||||
json={
|
||||
"values": values,
|
||||
"start": start,
|
||||
"duration": duration,
|
||||
"unit": unit
|
||||
},
|
||||
timeout=30
|
||||
)
|
||||
return r.status_code in [200, 201, 202]
|
||||
|
||||
# ========================================
|
||||
# 1. Données météo réelles via API Open-Meteo
|
||||
# ========================================
|
||||
print("=== Récupération des données météo ===")
|
||||
|
||||
LAT, LON = 14.6091, -61.2155
|
||||
today = datetime.now(MARTINIQUE_TZ).strftime("%Y-%m-%d")
|
||||
|
||||
resp = requests.get(
|
||||
f"https://api.open-meteo.com/v1/forecast?"
|
||||
f"latitude={LAT}&longitude={LON}"
|
||||
f"&hourly=shortwave_radiation,temperature_2m,wind_speed_10m"
|
||||
f"&timezone=America/Martinique"
|
||||
f"&start_date={today}&end_date={today}",
|
||||
timeout=15
|
||||
)
|
||||
|
||||
if resp.status_code == 200:
|
||||
wdata = resp.json()
|
||||
times = wdata["hourly"]["time"]
|
||||
|
||||
for sensor_name, (key, unit) in {
|
||||
"irradiance": ("shortwave_radiation", "W/m²"),
|
||||
"temperature": ("temperature_2m", "°C"),
|
||||
"wind_speed": ("wind_speed_10m", "m/s")
|
||||
}.items():
|
||||
sensor_id = SENSORS[sensor_name]
|
||||
values = wdata["hourly"][key]
|
||||
posted = 0
|
||||
|
||||
for i, t in enumerate(times):
|
||||
# Parse datetime and format properly
|
||||
dt = datetime.fromisoformat(t)
|
||||
dt_utc = dt.astimezone(timezone.utc)
|
||||
start_str = dt_utc.strftime("%Y-%m-%dT%H:%M:%S+00:00")
|
||||
val = round(max(0, values[i]), 1)
|
||||
|
||||
if post_sensor_data(sensor_id, [val], unit, start_str):
|
||||
posted += 1
|
||||
|
||||
print(f" '{sensor_name}': {posted}/{len(times)} posted")
|
||||
else:
|
||||
print(f" Erreur API: {resp.status_code}")
|
||||
|
||||
# ========================================
|
||||
# 2. Prix DSO simulés (24h)
|
||||
# ========================================
|
||||
print("\n=== Injection des prix DSO ===")
|
||||
|
||||
now_utc = datetime.now(timezone.utc)
|
||||
|
||||
for sensor_name, sensor_id in [("consumption_price", SENSORS["consumption_price"]),
|
||||
("production_price", SENSORS["production_price"])]:
|
||||
posted = 0
|
||||
for h in range(24):
|
||||
dt = now_utc - timedelta(hours=23-h)
|
||||
hour_of_day = dt.hour
|
||||
|
||||
if 6 <= hour_of_day <= 22:
|
||||
price = round(random.uniform(80, 150), 2) if "consumption" in sensor_name else round(random.uniform(60, 120), 2)
|
||||
else:
|
||||
price = round(random.uniform(40, 80), 2) if "consumption" in sensor_name else round(random.uniform(30, 60), 2)
|
||||
|
||||
start_str = dt.strftime("%Y-%m-%dT%H:%M:%S+00:00")
|
||||
if post_sensor_data(sensor_id, [price], "EUR/MWh", start_str):
|
||||
posted += 1
|
||||
|
||||
print(f" '{sensor_name}': {posted}/24 posted")
|
||||
|
||||
print("\n=== Terminé ===")
|
||||
97
scripts/inject_weather.py
Normal file
97
scripts/inject_weather.py
Normal file
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Cariflex - Corrige l'injection des données météo."""
|
||||
|
||||
import json
|
||||
import requests
|
||||
import re
|
||||
import warnings
|
||||
import random
|
||||
from datetime import datetime, timezone, timedelta
|
||||
import pytz
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
FM_HOST = "https://cariflex.digitribe.fr"
|
||||
CREDS_FILE = "/tmp/fm_creds.json"
|
||||
MARTINIQUE_TZ = pytz.timezone("America/Martinique")
|
||||
|
||||
with open(CREDS_FILE) as f:
|
||||
creds = json.load(f)
|
||||
|
||||
session = requests.Session()
|
||||
session.verify = False
|
||||
|
||||
# Login
|
||||
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": creds["email"], "password": creds["password"],
|
||||
"csrf_token": csrf, "remember": "y"
|
||||
}, allow_redirects=True)
|
||||
print(f"Login: {'OK' if 'dashboard' in r.url else 'FAILED'}")
|
||||
|
||||
# Sensor IDs (from previous creation)
|
||||
sensors = {
|
||||
"irradiance": 81,
|
||||
"temperature": 82,
|
||||
"wind_speed": 83,
|
||||
"consumption_price": 84,
|
||||
"production_price": 85,
|
||||
}
|
||||
|
||||
# Inject weather data
|
||||
print("\n=== Données météo réelles ===")
|
||||
LAT, LON = 14.6091, -61.2155
|
||||
today = datetime.now(MARTINIQUE_TZ).strftime("%Y-%m-%d")
|
||||
|
||||
resp = requests.get(
|
||||
f"https://api.open-meteo.com/v1/forecast?"
|
||||
f"latitude={LAT}&longitude={LON}"
|
||||
f"&hourly=shortwave_radiation,temperature_2m,wind_speed_10m"
|
||||
f"&timezone=America/Martinique"
|
||||
f"&start_date={today}&end_date={today}",
|
||||
timeout=15
|
||||
)
|
||||
|
||||
if resp.status_code == 200:
|
||||
wdata = resp.json()
|
||||
times = wdata["hourly"]["time"]
|
||||
|
||||
for sensor_name, (key, unit) in {
|
||||
"irradiance": ("shortwave_radiation", "W/m²"),
|
||||
"temperature": ("temperature_2m", "°C"),
|
||||
"wind_speed": ("wind_speed_10m", "m/s")
|
||||
}.items():
|
||||
sensor_id = sensors[sensor_name]
|
||||
values = wdata["hourly"][key]
|
||||
posted = 0
|
||||
|
||||
for i, t in enumerate(times):
|
||||
dt = datetime.fromisoformat(t)
|
||||
val = round(max(0, values[i]), 1)
|
||||
|
||||
try:
|
||||
r = session.post(
|
||||
f"{FM_HOST}/api/v3_0/sensors/{sensor_id}/data",
|
||||
json={
|
||||
"values": [val],
|
||||
"start": dt.isoformat(),
|
||||
"duration": "PT1H",
|
||||
"unit": unit
|
||||
},
|
||||
timeout=30
|
||||
)
|
||||
if r.status_code in [200, 201, 202]:
|
||||
posted += 1
|
||||
else:
|
||||
if posted == 0:
|
||||
print(f" Error response: {r.text[:100]}")
|
||||
except Exception as e:
|
||||
print(f" Exception: {e}")
|
||||
|
||||
print(f" '{sensor_name}': {posted}/{len(times)} posted")
|
||||
else:
|
||||
print(f" API error: {resp.status_code}")
|
||||
|
||||
print("\n=== Terminé ===")
|
||||
165
scripts/inject_weather_prices.py
Normal file
165
scripts/inject_weather_prices.py
Normal file
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Cariflex - Injecte les données météo réelles et prix DSO simulés."""
|
||||
|
||||
import json
|
||||
import requests
|
||||
import re
|
||||
import warnings
|
||||
import random
|
||||
from datetime import datetime, timezone, timedelta
|
||||
import pytz
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
FM_HOST = "https://cariflex.digitribe.fr"
|
||||
CREDS_FILE = "/tmp/fm_creds.json"
|
||||
MARTINIQUE_TZ = pytz.timezone("America/Martinique")
|
||||
|
||||
with open(CREDS_FILE) as f:
|
||||
creds = json.load(f)
|
||||
|
||||
session = requests.Session()
|
||||
session.verify = False
|
||||
|
||||
# Login
|
||||
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": creds["email"], "password": creds["password"],
|
||||
"csrf_token": csrf, "remember": "y"
|
||||
}, allow_redirects=True)
|
||||
print(f"Login: {'OK' if 'dashboard' in r.url else 'FAILED'}")
|
||||
|
||||
# Get existing sensors
|
||||
r = session.get(f"{FM_HOST}/api/v3_0/sensors")
|
||||
sensors = {}
|
||||
if r.status_code == 200:
|
||||
for s in r.json():
|
||||
sensors[s["name"]] = s["id"]
|
||||
print(f" Sensor: {s['id']} - {s['name']} (asset {s.get('generic_asset_id', '?')})")
|
||||
|
||||
# ========================================
|
||||
# 1. Injecter les prévisions météo réelles
|
||||
# ========================================
|
||||
print("\n=== Injection des données météo ===")
|
||||
|
||||
LAT, LON = 14.6091, -61.2155
|
||||
now_local = datetime.now(MARTINIQUE_TZ)
|
||||
today = now_local.strftime("%Y-%m-%d")
|
||||
|
||||
url = (
|
||||
f"https://api.open-meteo.com/v1/forecast?"
|
||||
f"latitude={LAT}&longitude={LON}"
|
||||
f"&hourly=shortwave_radiation,temperature_2m,wind_speed_10m"
|
||||
f"&timezone=America/Martinique"
|
||||
f"&start_date={today}&end_date={today}"
|
||||
)
|
||||
|
||||
try:
|
||||
resp = requests.get(url, timeout=15)
|
||||
if resp.status_code == 200:
|
||||
wdata = resp.json()
|
||||
hourly = wdata["hourly"]
|
||||
times = hourly["time"]
|
||||
irradiance = hourly["shortwave_radiation"]
|
||||
temperature = hourly["temperature_2m"]
|
||||
wind_speed = hourly["wind_speed_10m"]
|
||||
|
||||
print(f" Données météo récupérées: {len(times)} heures")
|
||||
|
||||
# Map sensor names to data
|
||||
sensor_data_map = {
|
||||
"irradiance": (irradiance, "W/m²"),
|
||||
"temperature": (temperature, "°C"),
|
||||
"wind_speed": (wind_speed, "m/s"),
|
||||
}
|
||||
|
||||
for sensor_name, (data, unit) in sensor_data_map.items():
|
||||
if sensor_name not in sensors:
|
||||
print(f" Sensor '{sensor_name}' non trouvé, ignoré")
|
||||
continue
|
||||
|
||||
sensor_id = sensors[sensor_name]
|
||||
posted = 0
|
||||
|
||||
for i, t in enumerate(times):
|
||||
dt = datetime.fromisoformat(t)
|
||||
value = max(0, data[i]) if data[i] is not None else 0
|
||||
|
||||
r = session.post(
|
||||
f"{FM_HOST}/api/v3_0/sensors/{sensor_id}/data",
|
||||
json={
|
||||
"values": [round(value, 1)],
|
||||
"start": dt.isoformat(),
|
||||
"duration": "PT1H",
|
||||
"unit": unit
|
||||
},
|
||||
timeout=30
|
||||
)
|
||||
if r.status_code in [200, 201, 202]:
|
||||
posted += 1
|
||||
|
||||
print(f" Sensor '{sensor_name}' (ID {sensor_id}): {posted}/{len(times)} values posted")
|
||||
else:
|
||||
print(f" Erreur API météo: HTTP {resp.status_code}")
|
||||
except Exception as e:
|
||||
print(f" Erreur récupération météo: {e}")
|
||||
|
||||
# ========================================
|
||||
# 2. Injecter les prix DSO simulés
|
||||
# ========================================
|
||||
print("\n=== Injection des prix DSO ===")
|
||||
|
||||
if "consumption_price" in sensors and "production_price" in sensors:
|
||||
consumption_price_id = sensors["consumption_price"]
|
||||
production_price_id = sensors["production_price"]
|
||||
|
||||
now_utc = datetime.now(timezone.utc)
|
||||
posted = 0
|
||||
|
||||
for hour in range(24):
|
||||
dt = now_utc - timedelta(hours=23-hour)
|
||||
hour_of_day = dt.hour
|
||||
|
||||
# Price pattern: peak during day, lower at night
|
||||
if 6 <= hour_of_day <= 22:
|
||||
consumption_price = round(random.uniform(80, 150), 2)
|
||||
production_price = round(random.uniform(60, 120), 2)
|
||||
else:
|
||||
consumption_price = round(random.uniform(40, 80), 2)
|
||||
production_price = round(random.uniform(30, 60), 2)
|
||||
|
||||
# Post consumption price
|
||||
r = session.post(
|
||||
f"{FM_HOST}/api/v3_0/sensors/{consumption_price_id}/data",
|
||||
json={
|
||||
"values": [consumption_price],
|
||||
"start": dt.isoformat(),
|
||||
"duration": "PT1H",
|
||||
"unit": "EUR/MWh"
|
||||
},
|
||||
timeout=30
|
||||
)
|
||||
if r.status_code in [200, 201, 202]:
|
||||
posted += 1
|
||||
|
||||
# Post production price
|
||||
r = session.post(
|
||||
f"{FM_HOST}/api/v3_0/sensors/{production_price_id}/data",
|
||||
json={
|
||||
"values": [production_price],
|
||||
"start": dt.isoformat(),
|
||||
"duration": "PT1H",
|
||||
"unit": "EUR/MWh"
|
||||
},
|
||||
timeout=30
|
||||
)
|
||||
if r.status_code in [200, 201, 202]:
|
||||
posted += 1
|
||||
|
||||
print(f" Prix DSO injectés: {posted} values")
|
||||
else:
|
||||
print(" Sensors de prix non trouvés")
|
||||
|
||||
print("\n=== Injection terminée ===")
|
||||
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)
|
||||
123
scripts/simulation_setup.py
Normal file
123
scripts/simulation_setup.py
Normal file
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Cariflex - Crée les sensors et injecte données météo/prix."""
|
||||
|
||||
import json
|
||||
import requests
|
||||
import re
|
||||
import warnings
|
||||
import random
|
||||
from datetime import datetime, timezone, timedelta
|
||||
import pytz
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
FM_HOST = "https://cariflex.digitribe.fr"
|
||||
CREDS_FILE = "/tmp/fm_creds.json"
|
||||
MARTINIQUE_TZ = pytz.timezone("America/Martinique")
|
||||
|
||||
with open(CREDS_FILE) as f:
|
||||
creds = json.load(f)
|
||||
|
||||
session = requests.Session()
|
||||
session.verify = False
|
||||
|
||||
# Login
|
||||
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": creds["email"], "password": creds["password"],
|
||||
"csrf_token": csrf, "remember": "y"
|
||||
}, allow_redirects=True)
|
||||
print(f"Login: {'OK' if 'dashboard' in r.url else 'FAILED'}")
|
||||
|
||||
RESOLUTION = "PT1H"
|
||||
|
||||
print("\n=== Création des sensors ===")
|
||||
sensors_to_create = [
|
||||
{"name": "irradiance", "unit": "W/m²", "generic_asset_id": 81, "event_resolution": RESOLUTION},
|
||||
{"name": "temperature", "unit": "°C", "generic_asset_id": 81, "event_resolution": RESOLUTION},
|
||||
{"name": "wind_speed", "unit": "m/s", "generic_asset_id": 81, "event_resolution": RESOLUTION},
|
||||
{"name": "consumption_price", "unit": "EUR/MWh", "generic_asset_id": 82, "event_resolution": RESOLUTION},
|
||||
{"name": "production_price", "unit": "EUR/MWh", "generic_asset_id": 82, "event_resolution": RESOLUTION},
|
||||
]
|
||||
|
||||
created_sensors = {}
|
||||
for s in sensors_to_create:
|
||||
r = session.post(f"{FM_HOST}/api/v3_0/sensors", json=s)
|
||||
if r.status_code == 201:
|
||||
sid = r.json().get("id")
|
||||
created_sensors[s["name"]] = sid
|
||||
print(f" ✓ Sensor '{s['name']}' (ID {sid})")
|
||||
|
||||
if not created_sensors:
|
||||
# Find existing sensors
|
||||
r = session.get(f"{FM_HOST}/api/v3_0/sensors")
|
||||
for s in r.json():
|
||||
if s["name"] in ["irradiance", "temperature", "wind_speed", "consumption_price", "production_price"]:
|
||||
created_sensors[s["name"]] = s["id"]
|
||||
print(f" Found sensor '{s['name']}' (ID {s['id']})")
|
||||
|
||||
print(f"\nSensors: {created_sensors}")
|
||||
|
||||
# Inject weather data from Open-Meteo
|
||||
print("\n=== Données météo réelles ===")
|
||||
LAT, LON = 14.6091, -61.2155
|
||||
today = datetime.now(MARTINIQUE_TZ).strftime("%Y-%m-%d")
|
||||
|
||||
try:
|
||||
resp = requests.get(
|
||||
f"https://api.open-meteo.com/v1/forecast?"
|
||||
f"latitude={LAT}&longitude={LON}"
|
||||
f"&hourly=shortwave_radiation,temperature_2m,wind_speed_10m"
|
||||
f"&timezone=America/Martinique"
|
||||
f"&start_date={today}&end_date={today}",
|
||||
timeout=15
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
wdata = resp.json()
|
||||
times = wdata["hourly"]["time"]
|
||||
|
||||
for sensor_name, (key, unit) in {
|
||||
"irradiance": ("shortwave_radiation", "W/m²"),
|
||||
"temperature": ("temperature_2m", "°C"),
|
||||
"wind_speed": ("wind_speed_10m", "m/s")
|
||||
}.items():
|
||||
if sensor_name not in created_sensors:
|
||||
continue
|
||||
sensor_id = created_sensors[sensor_name]
|
||||
values = wdata["hourly"][key]
|
||||
posted = 0
|
||||
for i, t in enumerate(times):
|
||||
dt = datetime.fromisoformat(t)
|
||||
r = session.post(f"{FM_HOST}/api/v3_0/sensors/{sensor_id}/data",
|
||||
json={"values": [round(max(0, values[i]), 1)], "start": dt.isoformat(), "duration": RESOLUTION, "unit": unit},
|
||||
timeout=30)
|
||||
if r.status_code in [200, 201, 202]:
|
||||
posted += 1
|
||||
print(f" '{sensor_name}': {posted}/{len(times)} posted")
|
||||
else:
|
||||
print(f" API error: {resp.status_code}")
|
||||
except Exception as e:
|
||||
print(f" Error: {e}")
|
||||
|
||||
# Inject DSO prices (24h)
|
||||
print("\n=== Prix DSO ===")
|
||||
if "consumption_price" in created_sensors and "production_price" in created_sensors:
|
||||
c_id = created_sensors["consumption_price"]
|
||||
p_id = created_sensors["production_price"]
|
||||
now_utc = datetime.now(timezone.utc)
|
||||
posted = 0
|
||||
for h in range(24):
|
||||
dt = now_utc - timedelta(hours=23-h)
|
||||
hd = dt.hour
|
||||
cp = round(random.uniform(80, 150) if 6 <= hd <= 22 else random.uniform(40, 80), 2)
|
||||
pp = round(random.uniform(60, 120) if 6 <= hd <= 22 else random.uniform(30, 60), 2)
|
||||
for sid, price in [(c_id, cp), (p_id, pp)]:
|
||||
r = session.post(f"{FM_HOST}/api/v3_0/sensors/{sid}/data",
|
||||
json={"values": [price], "start": dt.isoformat(), "duration": RESOLUTION, "unit": "EUR/MWh"}, timeout=30)
|
||||
if r.status_code in [200, 201, 202]:
|
||||
posted += 1
|
||||
print(f" Prix injectés: {posted}")
|
||||
|
||||
print("\n=== Terminé ===")
|
||||
Reference in New Issue
Block a user