diff --git a/config/cariflex-dashboard.json b/config/cariflex-dashboard.json index 61a5fcf..1fd2d2f 100644 --- a/config/cariflex-dashboard.json +++ b/config/cariflex-dashboard.json @@ -10,123 +10,79 @@ "panels": [ { "id": 1, - "title": "Capteurs Air Quality (10)", + "title": "Production PV (kW) - 10 panneaux", "type": "timeseries", "gridPos": {"h": 8, "w": 12, "x": 0, "y": 0}, "datasource": {"type": "influxdb", "uid": "influxdb-v2"}, "targets": [{ - "query": "from(bucket:\"smartcity\") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) |> filter(fn: (r) => r[\"_measurement\"] == \"mqtt_consumer\") |> filter(fn: (r) => r[\"topic\"] =~ /airquality/) |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)", + "query": "from(bucket:\"smartcity\") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) |> filter(fn: (r) => r[\"_measurement\"] == \"mqtt_consumer\") |> filter(fn: (r) => r[\"topic\"] =~ /pv_/) |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)", "refId": "A" }], - "fieldConfig": { - "defaults": {"unit": "none"}, - "overrides": [] - } + "fieldConfig": {"defaults": {"unit": "kW", "min": 0, "max": 50}} }, { "id": 2, - "title": "Capteurs Weather (10)", + "title": "Consommation Bornes VE (kW) - 10 bornes", "type": "timeseries", "gridPos": {"h": 8, "w": 12, "x": 12, "y": 0}, "datasource": {"type": "influxdb", "uid": "influxdb-v2"}, "targets": [{ - "query": "from(bucket:\"smartcity\") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) |> filter(fn: (r) => r[\"_measurement\"] == \"mqtt_consumer\") |> filter(fn: (r) => r[\"topic\"] =~ /weather/) |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)", + "query": "from(bucket:\"smartcity\") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) |> filter(fn: (r) => r[\"_measurement\"] == \"mqtt_consumer\") |> filter(fn: (r) => r[\"topic\"] =~ /chg_/) |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)", "refId": "A" }], - "fieldConfig": { - "defaults": {"unit": "celsius", "min": 15, "max": 40}, - "overrides": [] - } + "fieldConfig": {"defaults": {"unit": "kW", "min": 0, "max": 220}} }, { "id": 3, - "title": "Capteurs Traffic (10)", - "type": "timeseries", - "gridPos": {"h": 8, "w": 12, "x": 0, "y": 8}, + "title": "Batteries - État de Charge (kWh) - 10 batteries", + "type": "gauge", + "gridPos": {"h": 8, "w": 8, "x": 0, "y": 8}, "datasource": {"type": "influxdb", "uid": "influxdb-v2"}, "targets": [{ - "query": "from(bucket:\"smartcity\") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) |> filter(fn: (r) => r[\"_measurement\"] == \"mqtt_consumer\") |> filter(fn: (r) => r[\"topic\"] =~ /traffic/) |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)", + "query": "from(bucket:\"smartcity\") |> range(start: -5m) |> filter(fn: (r) => r[\"_measurement\"] == \"mqtt_consumer\") |> filter(fn: (r) => r[\"topic\"] =~ /bat_/) |> mean()", "refId": "A" }], "fieldConfig": { - "defaults": {"unit": "kmh", "min": 0, "max": 100}, - "overrides": [] + "defaults": {"unit": "kWh", "min": 0, "max": 100, "thresholds": {"steps": [{"color": "red", "value": 0}, {"color": "yellow", "value": 20}, {"color": "green", "value": 50}]}} } }, { "id": 4, - "title": "Capteurs Parking (10)", - "type": "timeseries", - "gridPos": {"h": 8, "w": 12, "x": 12, "y": 8}, + "title": "VE V2G - État de Charge (kWh) - 10 véhicules", + "type": "gauge", + "gridPos": {"h": 8, "w": 8, "x": 8, "y": 8}, "datasource": {"type": "influxdb", "uid": "influxdb-v2"}, "targets": [{ - "query": "from(bucket:\"smartcity\") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) |> filter(fn: (r) => r[\"_measurement\"] == \"mqtt_consumer\") |> filter(fn: (r) => r[\"topic\"] =~ /parking/) |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)", + "query": "from(bucket:\"smartcity\") |> range(start: -5m) |> filter(fn: (r) => r[\"_measurement\"] == \"mqtt_consumer\") |> filter(fn: (r) => r[\"topic\"] =~ /ev_/) |> mean()", "refId": "A" }], "fieldConfig": { - "defaults": {"unit": "percent", "min": 0, "max": 100}, - "overrides": [] + "defaults": {"unit": "kWh", "min": 0, "max": 75, "thresholds": {"steps": [{"color": "red", "value": 0}, {"color": "yellow", "value": 15}, {"color": "green", "value": 40}]}} } }, { "id": 5, - "title": "Battery Level (tous capteurs)", - "type": "gauge", - "gridPos": {"h": 6, "w": 6, "x": 0, "y": 16}, + "title": "Flexibilité Disponible (kW)", + "type": "stat", + "gridPos": {"h": 8, "w": 8, "x": 16, "y": 8}, "datasource": {"type": "influxdb", "uid": "influxdb-v2"}, "targets": [{ - "query": "from(bucket:\"smartcity\") |> range(start: -5m) |> filter(fn: (r) => r[\"_measurement\"] == \"mqtt_consumer\") |> filter(fn: (r) => r[\"_field\"] == \"battery_level\") |> mean()", + "query": "from(bucket:\"smartcity\") |> range(start: -5m) |> filter(fn: (r) => r[\"_measurement\"] == \"mqtt_consumer\") |> filter(fn: (r) => r[\"topic\"] =~ /bat_|ev_/) |> mean()", "refId": "A" }], - "fieldConfig": { - "defaults": {"unit": "percent", "min": 0, "max": 100, "thresholds": {"steps": [{"color": "red", "value": 0}, {"color": "yellow", "value": 20}, {"color": "green", "value": 50}]}}, - "overrides": [] - } + "fieldConfig": {"defaults": {"unit": "kW", "min": 0}} }, { "id": 6, - "title": "Temperature (°C)", - "type": "stat", - "gridPos": {"h": 6, "w": 6, "x": 6, "y": 16}, - "datasource": {"type": "influxdb", "uid": "influxdb-v2"}, + "title": "Carte des Actifs Cariflex", + "type": "geomap", + "gridPos": {"h": 10, "w": 24, "x": 0, "y": 16}, + "datasource": {"type": "postgres", "uid": "PostgreSQL-SmartCity"}, "targets": [{ - "query": "from(bucket:\"smartcity\") |> range(start: -5m) |> filter(fn: (r) => r[\"_measurement\"] == \"mqtt_consumer\") |> filter(fn: (r) => r[\"_field\"] == \"temperature_celsius\") |> mean()", + "rawSql": "SELECT g.name, g.latitude, g.longitude, gt.name as type FROM generic_asset g JOIN generic_asset_type gt ON g.generic_asset_type_id = gt.id WHERE g.account_id = 1 ORDER BY gt.id, g.id", "refId": "A" }], - "fieldConfig": { - "defaults": {"unit": "celsius", "min": 15, "max": 40}, - "overrides": [] - } - }, - { - "id": 7, - "title": "Noise Level (dB)", - "type": "stat", - "gridPos": {"h": 6, "w": 6, "x": 12, "y": 16}, - "datasource": {"type": "influxdb", "uid": "influxdb-v2"}, - "targets": [{ - "query": "from(bucket:\"smartcity\") |> range(start: -5m) |> filter(fn: (r) => r[\"_measurement\"] == \"mqtt_consumer\") |> filter(fn: (r) => r[\"_field\"] == \"noise_level_db\") |> mean()", - "refId": "A" - }], - "fieldConfig": { - "defaults": {"unit": "dB", "min": 0, "max": 120}, - "overrides": [] - } - }, - { - "id": 8, - "title": "Rain (mm)", - "type": "stat", - "gridPos": {"h": 6, "w": 6, "x": 18, "y": 16}, - "datasource": {"type": "influxdb", "uid": "influxdb-v2"}, - "targets": [{ - "query": "from(bucket:\"smartcity\") |> range(start: -1h) |> filter(fn: (r) => r[\"_measurement\"] == \"mqtt_consumer\") |> filter(fn: (r) => r[\"_field\"] == \"rain_mm\") |> sum()", - "refId": "A" - }], - "fieldConfig": { - "defaults": {"unit": "mm", "min": 0}, - "overrides": [] - } + "options": {"view": {"center": [14.6, -61.2], "zoom": 10}} } ] }, diff --git a/scripts/cariflex_simulator.py b/scripts/cariflex_simulator.py index 07b342e..0b2c5e7 100644 --- a/scripts/cariflex_simulator.py +++ b/scripts/cariflex_simulator.py @@ -1,137 +1,110 @@ #!/usr/bin/env python3 """ -Cariflex Simulator - Publishes simulated EV charging data to Redis. +Cariflex Simulator - Publishes simulated data to FlexMeasures API. +Uses flexmeasures_client for authentication and data posting. Simulates 40 assets: 10 PV, 10 Battery, 10 EV Charger, 10 EV V2G """ -import redis -import json +import asyncio import time import random import math -from datetime import datetime, timezone +from datetime import datetime, timezone, timedelta +from flexmeasures_client import FlexMeasuresClient -# Redis connection -r = redis.Redis(host='flexmeasures-redis', port=6379, db=0, decode_responses=True) +FM_HOST = "https://flexmeasures.digitribe.fr" +FM_EMAIL = "admin@digitribe.fr" +FM_PASSWORD = "Digitribe972" -# Asset configurations -ASSETS = { - # PV panels (production) - "pv_{:02d}": {"type": "pv", "unit": "kW", "min": 0, "max": 5, "base": 2.5}, - # Batteries (storage) - "bat_{:02d}": {"type": "battery", "unit": "kWh", "min": 10, "max": 100, "base": 50}, - # EV Chargers (consumption) - "chg_{:02d}": {"type": "ev_charger", "unit": "kW", "min": 0, "max": 22, "base": 11}, - # EVs (V2G - bidirectional) - "ev_{:02d}": {"type": "ev_v2g", "unit": "kW", "min": -11, "max": 11, "base": 0}, -} +# Sensor mapping: sensor_id -> (name, type, unit, min, max) +SENSORS = {} +for i in range(1, 11): + SENSORS[40 + i] = {"name": f"pv_{i:02d}_power", "type": "pv", "unit": "kW", "min": 0, "max": 5} +for i in range(1, 11): + SENSORS[50 + i] = {"name": f"bat_{i:02d}_power", "type": "battery", "unit": "kWh", "min": 10, "max": 100} +for i in range(1, 11): + SENSORS[60 + i] = {"name": f"chg_{i:02d}_power", "type": "ev_charger", "unit": "kW", "min": 0, "max": 22} +for i in range(1, 11): + SENSORS[70 + i] = {"name": f"ev_{i:02d}_power", "type": "ev_v2g", "unit": "kWh", "min": 15, "max": 75} -def generate_value(asset_config, hour): - """Generate a realistic value based on asset type and time of day.""" - cfg = asset_config - base = cfg["base"] - - if cfg["type"] == "pv": - # Solar production: peaks at noon - solar_factor = max(0, math.sin((hour - 6) * math.pi / 12)) if 6 <= hour <= 18 else 0 - noise = random.gauss(0, 0.5) - value = base * solar_factor * 2 + noise - elif cfg["type"] == "battery": - # SOC: slowly varies throughout the day - variation = 20 * math.sin(hour * math.pi / 12) - noise = random.gauss(0, 3) - value = base + variation + noise - elif cfg["type"] == "ev_charger": - # Charging: more active during day and evening +def generate_value(cfg, hour): + """Generate realistic value based on asset type and time of day.""" + t = cfg["type"] + if t == "pv": + if 6 <= hour <= 18: + factor = max(0, math.sin((hour - 6) * math.pi / 12)) + else: + factor = 0 + return round(max(0, cfg["max"] * factor + random.gauss(0, 0.3)), 2) + elif t == "battery": + base = 50 + 30 * math.sin((hour - 6) * math.pi / 12) + return round(max(cfg["min"], min(cfg["max"], base + random.gauss(0, 2))), 2) + elif t == "ev_charger": if 8 <= hour <= 22: - factor = random.uniform(0.3, 1.0) + factor = random.uniform(0.2, 1.0) else: - factor = random.uniform(0, 0.2) - noise = random.gauss(0, 1) - value = cfg["max"] * factor + noise - elif cfg["type"] == "ev_v2g": - # V2G: charges at night, discharges during peak + factor = random.uniform(0, 0.15) + return round(max(0, cfg["max"] * factor + random.gauss(0, 0.5)), 2) + elif t == "ev_v2g": if 0 <= hour <= 6: - factor = random.uniform(0.3, 0.8) # charging + base = 60 elif 17 <= hour <= 21: - factor = random.uniform(-0.6, -0.2) # discharging + base = 30 else: - factor = random.uniform(-0.1, 0.1) - noise = random.gauss(0, 0.5) - value = cfg["max"] * factor + noise - else: - value = base + random.gauss(0, 1) - - return round(max(cfg["min"], min(cfg["max"], value)), 2) + base = 45 + return round(max(cfg["min"], min(cfg["max"], base + random.gauss(0, 3))), 2) + return 0 -def main(): - print("🚗 Cariflex Simulator - Publishing to Redis") - print(f" Assets: 40 (10 PV, 10 Bat, 10 Chg, 10 EV)") - print(f" Redis: flexmeasures-redis:6379/0") +async def post_all_data(client): + """Post data for all 40 sensors.""" + now = datetime.now(timezone.utc) + hour = now.hour + start = now - timedelta(minutes=5) + + success = 0 + failed = 0 + + for sensor_id, cfg in SENSORS.items(): + value = generate_value(cfg, hour) + try: + await client.post_sensor_data( + sensor_id=sensor_id, + values=[value], + start=start.isoformat(), + duration="PT5M", + unit=cfg["unit"] + ) + success += 1 + except Exception as e: + failed += 1 + if failed <= 3: + print(f" ⚠️ Sensor {sensor_id}: {e}") + + return success, failed + +async def main(): + print("🚗 Cariflex Simulator → FlexMeasures API") + print(f" Sensors: {len(SENSORS)} (10 PV, 10 Bat, 10 Chg, 10 EV)") + print(f" FM API: {FM_HOST}") print() - # Test Redis connection - try: - r.ping() - print("✅ Redis connected") - except redis.ConnectionError: - print("❌ Redis connection failed") - return + client = FlexMeasuresClient( + email=FM_EMAIL, + password=FM_PASSWORD, + host="flexmeasures.digitribe.fr", + ssl=True, + request_timeout=60.0 + ) + + print("✅ Connected to FlexMeasures") - # Publish loop iteration = 0 while True: - now = datetime.now(timezone.utc) - hour = now.hour - timestamp = now.isoformat() - - # Publish each asset's data - for template, cfg in ASSETS.items(): - for i in range(1, 11): - asset_id = template.format(i) - value = generate_value(cfg, hour) - - # Create data packet - data = { - "asset_id": asset_id, - "type": cfg["type"], - "value": value, - "unit": cfg["unit"], - "timestamp": timestamp, - "iteration": iteration - } - - # Publish to Redis (list per asset) - key = f"cariflex:asset:{asset_id}" - r.lpush(key, json.dumps(data)) - r.ltrim(key, 0, 99) # Keep last 100 values - r.expire(key, 3600) # 1h TTL - - # Also publish to a pub/sub channel - r.publish("cariflex:data", json.dumps(data)) - - # Publish aggregate data - aggregate = { - "timestamp": timestamp, - "total_pv_kw": sum(generate_value({"type": "pv", "base": 2.5, "min": 0, "max": 5}, hour) for _ in range(10)), - "total_battery_soc": sum(generate_value({"type": "battery", "base": 50, "min": 10, "max": 100}, hour) for _ in range(10)) / 10, - "total_charger_kw": sum(generate_value({"type": "ev_charger", "base": 11, "min": 0, "max": 22}, hour) for _ in range(10)), - "total_ev_v2g_kw": sum(generate_value({"type": "ev_v2g", "base": 0, "min": -11, "max": 11}, hour) for _ in range(10)), - "flexibility_available_kw": 0 # Will be calculated - } - aggregate["flexibility_available_kw"] = round( - abs(aggregate["total_ev_v2g_kw"]) + - abs(aggregate["total_charger_kw"] * 0.3) + # 30% of charger can be modulated - abs(aggregate["total_battery_soc"] * 0.5), # 50% of battery capacity - 2 - ) - - r.set("cariflex:aggregate", json.dumps(aggregate), ex=300) - + success, failed = await post_all_data(client) iteration += 1 - if iteration % 10 == 0: - print(f" 📊 Iteration {iteration}: published {40} assets to Redis") + now = datetime.now(timezone.utc) + print(f" 📊 Iteration {iteration}: {success} OK, {failed} failed (hour={now.hour})") - time.sleep(10) # Publish every 10 seconds + await asyncio.sleep(30) if __name__ == "__main__": - main() + asyncio.run(main())