From d1b57769c00ad3cb3dcb8cc3b1db9a85b12ea55a Mon Sep 17 00:00:00 2001 From: Eric F Date: Tue, 9 Jun 2026 18:59:37 -0400 Subject: [PATCH] Grafana dashboard flexibilite/agregation --- scripts/create_sim_assets.py | 78 ++++++++ scripts/create_simulation_assets.py | 92 +++++++++ scripts/inject_final.py | 132 ++++++++++++ scripts/inject_weather.py | 97 +++++++++ scripts/inject_weather_prices.py | 165 +++++++++++++++ scripts/simulation_dashboard.py | 299 ++++++++++++++++++++++++++++ scripts/simulation_setup.py | 123 ++++++++++++ 7 files changed, 986 insertions(+) create mode 100644 scripts/create_sim_assets.py create mode 100644 scripts/create_simulation_assets.py create mode 100644 scripts/inject_final.py create mode 100644 scripts/inject_weather.py create mode 100644 scripts/inject_weather_prices.py create mode 100644 scripts/simulation_dashboard.py create mode 100644 scripts/simulation_setup.py diff --git a/scripts/create_sim_assets.py b/scripts/create_sim_assets.py new file mode 100644 index 0000000..d2d0d8f --- /dev/null +++ b/scripts/create_sim_assets.py @@ -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']*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}") diff --git a/scripts/create_simulation_assets.py b/scripts/create_simulation_assets.py new file mode 100644 index 0000000..fa88d37 --- /dev/null +++ b/scripts/create_simulation_assets.py @@ -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']*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 ===") diff --git a/scripts/inject_final.py b/scripts/inject_final.py new file mode 100644 index 0000000..9ad5945 --- /dev/null +++ b/scripts/inject_final.py @@ -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']*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é ===") diff --git a/scripts/inject_weather.py b/scripts/inject_weather.py new file mode 100644 index 0000000..a6a3669 --- /dev/null +++ b/scripts/inject_weather.py @@ -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']*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é ===") diff --git a/scripts/inject_weather_prices.py b/scripts/inject_weather_prices.py new file mode 100644 index 0000000..dc4e8b3 --- /dev/null +++ b/scripts/inject_weather_prices.py @@ -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']*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 ===") diff --git a/scripts/simulation_dashboard.py b/scripts/simulation_dashboard.py new file mode 100644 index 0000000..17dd564 --- /dev/null +++ b/scripts/simulation_dashboard.py @@ -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 = """ + + + + Cariflex EMS - Simulation + + + + + + +
+

🌴 Cariflex EMS - Simulation Temps Réel

+ +
+
+
+
--
+
Production PV (kW)
+
+
+
+
+
--
+
SOC Batteries (kWh)
+
+
+
+
+
--
+
Charge VE (kW)
+
+
+
+
+
--
+
Prix DSO (EUR/MWh)
+
+
+
+ +
+
+
+
--
+
Irradiance (W/m²)
+
+
+
+
+
--
+
Température (°C)
+
+
+
+
+
--
+
Flexibilité (kW)
+
+
+
+
+
--
+
Balance Réseau (kW)
+
+
+
+ +
+
+
+
+ 📊 Données en temps réel + +
+
+
Chargement...
+
+
+
+
+
+
⚙️ Paramètres
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+
+
+ + + + +""" + +# 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']*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": "
".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) diff --git a/scripts/simulation_setup.py b/scripts/simulation_setup.py new file mode 100644 index 0000000..76faa54 --- /dev/null +++ b/scripts/simulation_setup.py @@ -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']*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é ===")