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:
@@ -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": []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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."""
|
||||||
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")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Test Redis connection
|
|
||||||
try:
|
|
||||||
r.ping()
|
|
||||||
print("✅ Redis connected")
|
|
||||||
except redis.ConnectionError:
|
|
||||||
print("❌ Redis connection failed")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Publish loop
|
|
||||||
iteration = 0
|
|
||||||
while True:
|
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
hour = now.hour
|
hour = now.hour
|
||||||
timestamp = now.isoformat()
|
start = now - timedelta(minutes=5)
|
||||||
|
|
||||||
# Publish each asset's data
|
success = 0
|
||||||
for template, cfg in ASSETS.items():
|
failed = 0
|
||||||
for i in range(1, 11):
|
|
||||||
asset_id = template.format(i)
|
for sensor_id, cfg in SENSORS.items():
|
||||||
value = generate_value(cfg, hour)
|
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}")
|
||||||
|
|
||||||
# Create data packet
|
return success, failed
|
||||||
data = {
|
|
||||||
"asset_id": asset_id,
|
|
||||||
"type": cfg["type"],
|
|
||||||
"value": value,
|
|
||||||
"unit": cfg["unit"],
|
|
||||||
"timestamp": timestamp,
|
|
||||||
"iteration": iteration
|
|
||||||
}
|
|
||||||
|
|
||||||
# Publish to Redis (list per asset)
|
async def main():
|
||||||
key = f"cariflex:asset:{asset_id}"
|
print("🚗 Cariflex Simulator → FlexMeasures API")
|
||||||
r.lpush(key, json.dumps(data))
|
print(f" Sensors: {len(SENSORS)} (10 PV, 10 Bat, 10 Chg, 10 EV)")
|
||||||
r.ltrim(key, 0, 99) # Keep last 100 values
|
print(f" FM API: {FM_HOST}")
|
||||||
r.expire(key, 3600) # 1h TTL
|
print()
|
||||||
|
|
||||||
# Also publish to a pub/sub channel
|
client = FlexMeasuresClient(
|
||||||
r.publish("cariflex:data", json.dumps(data))
|
email=FM_EMAIL,
|
||||||
|
password=FM_PASSWORD,
|
||||||
# Publish aggregate data
|
host="flexmeasures.digitribe.fr",
|
||||||
aggregate = {
|
ssl=True,
|
||||||
"timestamp": timestamp,
|
request_timeout=60.0
|
||||||
"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)
|
print("✅ Connected to FlexMeasures")
|
||||||
|
|
||||||
|
iteration = 0
|
||||||
|
while True:
|
||||||
|
success, failed = await post_all_data(client)
|
||||||
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())
|
||||||
|
|||||||
Reference in New Issue
Block a user