171 lines
5.6 KiB
Python
171 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Cariflex - OpenLEADR VEN (Virtual End Node)
|
|
Reçoit les signaux DSR du TSO/DSO et les transmet à FlexMeasures.
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
from datetime import datetime, timezone, timedelta
|
|
from openleadr import OpenADRClient, enable_default_logging
|
|
import requests
|
|
import re
|
|
|
|
# Configuration
|
|
FM_HOST = "https://cariflex.digitribe.fr"
|
|
FM_EMAIL = "admin@digitribe.fr"
|
|
FM_PASSWORD = "Digitribe972"
|
|
VEN_NAME = "Cariflex-VEN"
|
|
VTN_URL = "http://localhost:8080" # URL du VTN OpenADR (à configurer)
|
|
|
|
# Sensors IDs dans FM
|
|
SENSOR_CONSUMPTION_PRICE = 84
|
|
SENSOR_PRODUCTION_PRICE = 85
|
|
SENSOR_PV_FORECAST = 41 # PV_01
|
|
SENSOR_BAT_SOC = 51 # Bat_01
|
|
|
|
# Setup logging
|
|
enable_default_logging()
|
|
logger = logging.getLogger("openleadr")
|
|
|
|
class CariflexVEN:
|
|
"""OpenLEADR VEN pour Cariflex EMS."""
|
|
|
|
def __init__(self):
|
|
self.client = OpenADRClient(
|
|
vad_name=VEN_NAME,
|
|
vtn_url=VTN_URL,
|
|
)
|
|
self.fm_session = None
|
|
|
|
def fm_login(self):
|
|
"""Login to FlexMeasures."""
|
|
self.fm_session = requests.Session()
|
|
self.fm_session.verify = False
|
|
r = self.fm_session.get(f"{FM_HOST}/login")
|
|
match = re.search(r'<input[^>]*csrf_token[^>]*value="([^"]+)"', r.text)
|
|
if match:
|
|
csrf = match.group(1)
|
|
r = self.fm_session.post(f"{FM_HOST}/login", data={
|
|
"email": FM_EMAIL, "password": FM_PASSWORD,
|
|
"csrf_token": csrf, "remember": "y"
|
|
}, allow_redirects=True)
|
|
return "dashboard" in r.url
|
|
return False
|
|
|
|
def post_sensor_data(self, sensor_id, values, unit, start, duration="PT1H"):
|
|
"""Post sensor data to FlexMeasures."""
|
|
if not self.fm_session:
|
|
if not self.fm_login():
|
|
return False
|
|
|
|
r = self.fm_session.post(
|
|
f"{FM_HOST}/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]
|
|
|
|
async def handle_dsr_event(self, event):
|
|
"""
|
|
Handle DSR event from TSO/DSO.
|
|
|
|
Event format:
|
|
{
|
|
"event_id": "...",
|
|
"modification_number": 0,
|
|
"event_status": "active",
|
|
"created_date_time": "...",
|
|
"event_descriptor": {...},
|
|
"active_period": {...},
|
|
"event_signals": [
|
|
{
|
|
"signal_name": "LOAD_CONTROL",
|
|
"signal_type": "X_LOAD_CONTROL",
|
|
"intervals": [
|
|
{"duration": "PT1H", "signal_payload": 0.5}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
"""
|
|
logger.info(f"Received DSR event: {event.get('event_id')}")
|
|
|
|
for signal in event.get("event_signals", []):
|
|
signal_name = signal.get("signal_name")
|
|
signal_type = signal.get("signal_type")
|
|
|
|
for interval in signal.get("intervals", []):
|
|
payload = interval.get("signal_payload")
|
|
duration = interval.get("duration", "PT1H")
|
|
|
|
# Convert payload to price signal
|
|
# payload: 0.0 = low price, 1.0 = high price
|
|
if signal_name in ["LOAD_CONTROL", "PRICE"]:
|
|
# Calculate price based on payload
|
|
base_price = 100 # EUR/MWh
|
|
price = base_price * (0.5 + payload) # 50-150 EUR/MWh
|
|
|
|
# Post to FM
|
|
now = datetime.now(timezone.utc)
|
|
success = self.post_sensor_data(
|
|
SENSOR_CONSUMPTION_PRICE,
|
|
[round(price, 2)],
|
|
"EUR/MWh",
|
|
now.isoformat(),
|
|
duration
|
|
)
|
|
logger.info(f"Posted price {price} EUR/MWh: {success}")
|
|
|
|
elif signal_name == "RENEWABLE_GENERATION":
|
|
# Adjust PV forecast based on DSR signal
|
|
# payload: 0.0 = no PV, 1.0 = max PV
|
|
pv_factor = payload
|
|
# TODO: Adjust PV forecast
|
|
logger.info(f"PV factor: {pv_factor}")
|
|
|
|
return "OK"
|
|
|
|
async def handle_s2_message(self, message):
|
|
"""
|
|
Handle S2 message from TSO/DSO.
|
|
|
|
S2 messages are used for real-time control of flexible resources.
|
|
"""
|
|
logger.info(f"Received S2 message: {message.get('message_type')}")
|
|
|
|
msg_type = message.get("message_type")
|
|
|
|
if msg_type == "FRBC.ActuatorStatus":
|
|
# Update actuator status in FM
|
|
pass
|
|
elif msg_type == "FRBC.Instruction":
|
|
# Execute instruction in FM
|
|
pass
|
|
elif msg_type == "FRBC.SystemDescription":
|
|
# Update system description in FM
|
|
pass
|
|
|
|
return "OK"
|
|
|
|
async def run(self):
|
|
"""Run the OpenLEADR VEN."""
|
|
logger.info(f"Starting Cariflex VEN: {VEN_NAME}")
|
|
|
|
# Register event handler
|
|
self.client.add_handler("on_event", self.handle_dsr_event)
|
|
|
|
# Login to FM
|
|
if self.fm_login():
|
|
logger.info("Logged in to FlexMeasures")
|
|
else:
|
|
logger.error("Failed to login to FlexMeasures")
|
|
|
|
# Start the VEN
|
|
await self.client.run()
|
|
|
|
if __name__ == "__main__":
|
|
ven = CariflexVEN()
|
|
asyncio.run(ven.run())
|