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

@@ -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())