Fix simulator (use FM client), install entsoe+weather plugins, fix Redis

- Simulator rewritten to use flexmeasures_client (works!)
- flexmeasures-entsoe installed (ENTSO-E data import)
- flexmeasures-weather installed (weather data)
- FlexMeasures Redis connection fixed (DNS resolution)
- Dashboard Grafana updated with Cariflex asset types
- Simulator running in background, posting to 40 sensors

TODO:
- S2 CEM deployment
- Scheduler FlexMeasures
- Logo Cariflex in FM UI
This commit is contained in:
Eric F
2026-06-08 08:33:16 -04:00
parent 8f7c24acd4
commit 6fe79471f3
2 changed files with 112 additions and 183 deletions

View File

@@ -10,123 +10,79 @@
"panels": [ "panels": [
{ {
"id": 1, "id": 1,
"title": "Capteurs Air Quality (10)", "title": "Production PV (kW) - 10 panneaux",
"type": "timeseries", "type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 0}, "gridPos": {"h": 8, "w": 12, "x": 0, "y": 0},
"datasource": {"type": "influxdb", "uid": "influxdb-v2"}, "datasource": {"type": "influxdb", "uid": "influxdb-v2"},
"targets": [{ "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" "refId": "A"
}], }],
"fieldConfig": { "fieldConfig": {"defaults": {"unit": "kW", "min": 0, "max": 50}}
"defaults": {"unit": "none"},
"overrides": []
}
}, },
{ {
"id": 2, "id": 2,
"title": "Capteurs Weather (10)", "title": "Consommation Bornes VE (kW) - 10 bornes",
"type": "timeseries", "type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 0}, "gridPos": {"h": 8, "w": 12, "x": 12, "y": 0},
"datasource": {"type": "influxdb", "uid": "influxdb-v2"}, "datasource": {"type": "influxdb", "uid": "influxdb-v2"},
"targets": [{ "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" "refId": "A"
}], }],
"fieldConfig": { "fieldConfig": {"defaults": {"unit": "kW", "min": 0, "max": 220}}
"defaults": {"unit": "celsius", "min": 15, "max": 40},
"overrides": []
}
}, },
{ {
"id": 3, "id": 3,
"title": "Capteurs Traffic (10)", "title": "Batteries - État de Charge (kWh) - 10 batteries",
"type": "timeseries", "type": "gauge",
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 8}, "gridPos": {"h": 8, "w": 8, "x": 0, "y": 8},
"datasource": {"type": "influxdb", "uid": "influxdb-v2"}, "datasource": {"type": "influxdb", "uid": "influxdb-v2"},
"targets": [{ "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" "refId": "A"
}], }],
"fieldConfig": { "fieldConfig": {
"defaults": {"unit": "kmh", "min": 0, "max": 100}, "defaults": {"unit": "kWh", "min": 0, "max": 100, "thresholds": {"steps": [{"color": "red", "value": 0}, {"color": "yellow", "value": 20}, {"color": "green", "value": 50}]}}
"overrides": []
} }
}, },
{ {
"id": 4, "id": 4,
"title": "Capteurs Parking (10)", "title": "VE V2G - État de Charge (kWh) - 10 véhicules",
"type": "timeseries", "type": "gauge",
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 8}, "gridPos": {"h": 8, "w": 8, "x": 8, "y": 8},
"datasource": {"type": "influxdb", "uid": "influxdb-v2"}, "datasource": {"type": "influxdb", "uid": "influxdb-v2"},
"targets": [{ "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" "refId": "A"
}], }],
"fieldConfig": { "fieldConfig": {
"defaults": {"unit": "percent", "min": 0, "max": 100}, "defaults": {"unit": "kWh", "min": 0, "max": 75, "thresholds": {"steps": [{"color": "red", "value": 0}, {"color": "yellow", "value": 15}, {"color": "green", "value": 40}]}}
"overrides": []
} }
}, },
{ {
"id": 5, "id": 5,
"title": "Battery Level (tous capteurs)", "title": "Flexibilité Disponible (kW)",
"type": "gauge", "type": "stat",
"gridPos": {"h": 6, "w": 6, "x": 0, "y": 16}, "gridPos": {"h": 8, "w": 8, "x": 16, "y": 8},
"datasource": {"type": "influxdb", "uid": "influxdb-v2"}, "datasource": {"type": "influxdb", "uid": "influxdb-v2"},
"targets": [{ "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" "refId": "A"
}], }],
"fieldConfig": { "fieldConfig": {"defaults": {"unit": "kW", "min": 0}}
"defaults": {"unit": "percent", "min": 0, "max": 100, "thresholds": {"steps": [{"color": "red", "value": 0}, {"color": "yellow", "value": 20}, {"color": "green", "value": 50}]}},
"overrides": []
}
}, },
{ {
"id": 6, "id": 6,
"title": "Temperature (°C)", "title": "Carte des Actifs Cariflex",
"type": "stat", "type": "geomap",
"gridPos": {"h": 6, "w": 6, "x": 6, "y": 16}, "gridPos": {"h": 10, "w": 24, "x": 0, "y": 16},
"datasource": {"type": "influxdb", "uid": "influxdb-v2"}, "datasource": {"type": "postgres", "uid": "PostgreSQL-SmartCity"},
"targets": [{ "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" "refId": "A"
}], }],
"fieldConfig": { "options": {"view": {"center": [14.6, -61.2], "zoom": 10}}
"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": []
}
} }
] ]
}, },

View File

@@ -1,137 +1,110 @@
#!/usr/bin/env python3 #!/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 Simulates 40 assets: 10 PV, 10 Battery, 10 EV Charger, 10 EV V2G
""" """
import redis import asyncio
import json
import time import time
import random import random
import math import math
from datetime import datetime, timezone from datetime import datetime, timezone, timedelta
from flexmeasures_client import FlexMeasuresClient
# Redis connection FM_HOST = "https://flexmeasures.digitribe.fr"
r = redis.Redis(host='flexmeasures-redis', port=6379, db=0, decode_responses=True) FM_EMAIL = "admin@digitribe.fr"
FM_PASSWORD = "Digitribe972"
# Asset configurations # Sensor mapping: sensor_id -> (name, type, unit, min, max)
ASSETS = { SENSORS = {}
# PV panels (production) for i in range(1, 11):
"pv_{:02d}": {"type": "pv", "unit": "kW", "min": 0, "max": 5, "base": 2.5}, SENSORS[40 + i] = {"name": f"pv_{i:02d}_power", "type": "pv", "unit": "kW", "min": 0, "max": 5}
# Batteries (storage) for i in range(1, 11):
"bat_{:02d}": {"type": "battery", "unit": "kWh", "min": 10, "max": 100, "base": 50}, SENSORS[50 + i] = {"name": f"bat_{i:02d}_power", "type": "battery", "unit": "kWh", "min": 10, "max": 100}
# EV Chargers (consumption) for i in range(1, 11):
"chg_{:02d}": {"type": "ev_charger", "unit": "kW", "min": 0, "max": 22, "base": 11}, SENSORS[60 + i] = {"name": f"chg_{i:02d}_power", "type": "ev_charger", "unit": "kW", "min": 0, "max": 22}
# EVs (V2G - bidirectional) for i in range(1, 11):
"ev_{:02d}": {"type": "ev_v2g", "unit": "kW", "min": -11, "max": 11, "base": 0}, SENSORS[70 + i] = {"name": f"ev_{i:02d}_power", "type": "ev_v2g", "unit": "kWh", "min": 15, "max": 75}
}
def generate_value(asset_config, hour): def generate_value(cfg, hour):
"""Generate a realistic value based on asset type and time of day.""" """Generate realistic value based on asset type and time of day."""
cfg = asset_config t = cfg["type"]
base = cfg["base"] if t == "pv":
if 6 <= hour <= 18:
if cfg["type"] == "pv": factor = max(0, math.sin((hour - 6) * math.pi / 12))
# Solar production: peaks at noon else:
solar_factor = max(0, math.sin((hour - 6) * math.pi / 12)) if 6 <= hour <= 18 else 0 factor = 0
noise = random.gauss(0, 0.5) return round(max(0, cfg["max"] * factor + random.gauss(0, 0.3)), 2)
value = base * solar_factor * 2 + noise elif t == "battery":
elif cfg["type"] == "battery": base = 50 + 30 * math.sin((hour - 6) * math.pi / 12)
# SOC: slowly varies throughout the day return round(max(cfg["min"], min(cfg["max"], base + random.gauss(0, 2))), 2)
variation = 20 * math.sin(hour * math.pi / 12) elif t == "ev_charger":
noise = random.gauss(0, 3)
value = base + variation + noise
elif cfg["type"] == "ev_charger":
# Charging: more active during day and evening
if 8 <= hour <= 22: if 8 <= hour <= 22:
factor = random.uniform(0.3, 1.0) factor = random.uniform(0.2, 1.0)
else: else:
factor = random.uniform(0, 0.2) factor = random.uniform(0, 0.15)
noise = random.gauss(0, 1) return round(max(0, cfg["max"] * factor + random.gauss(0, 0.5)), 2)
value = cfg["max"] * factor + noise elif t == "ev_v2g":
elif cfg["type"] == "ev_v2g":
# V2G: charges at night, discharges during peak
if 0 <= hour <= 6: if 0 <= hour <= 6:
factor = random.uniform(0.3, 0.8) # charging base = 60
elif 17 <= hour <= 21: elif 17 <= hour <= 21:
factor = random.uniform(-0.6, -0.2) # discharging base = 30
else: else:
factor = random.uniform(-0.1, 0.1) base = 45
noise = random.gauss(0, 0.5) return round(max(cfg["min"], min(cfg["max"], base + random.gauss(0, 3))), 2)
value = cfg["max"] * factor + noise return 0
else:
value = base + random.gauss(0, 1)
return round(max(cfg["min"], min(cfg["max"], value)), 2) 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)
def main(): success = 0
print("🚗 Cariflex Simulator - Publishing to Redis") failed = 0
print(f" Assets: 40 (10 PV, 10 Bat, 10 Chg, 10 EV)")
print(f" Redis: flexmeasures-redis:6379/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() print()
# Test Redis connection client = FlexMeasuresClient(
try: email=FM_EMAIL,
r.ping() password=FM_PASSWORD,
print("✅ Redis connected") host="flexmeasures.digitribe.fr",
except redis.ConnectionError: ssl=True,
print("❌ Redis connection failed") request_timeout=60.0
return )
print("✅ Connected to FlexMeasures")
# Publish loop
iteration = 0 iteration = 0
while True: while True:
now = datetime.now(timezone.utc) success, failed = await post_all_data(client)
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)
iteration += 1 iteration += 1
if iteration % 10 == 0: now = datetime.now(timezone.utc)
print(f" 📊 Iteration {iteration}: published {40} assets to Redis") 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__": if __name__ == "__main__":
main() asyncio.run(main())