247 lines
7.8 KiB
Python
247 lines
7.8 KiB
Python
![]() |
#!/usr/bin/env python3
|
||
|
"""
|
||
|
EDF Schedules Extractor for the OpenEventDatabase.
|
||
|
|
||
|
This script fetches nuclear power plant maintenance schedules from the EDF open data API
|
||
|
and adds them to the OpenEventDatabase.
|
||
|
|
||
|
API URL: https://opendata.edf.fr/api/explore/v2.1/catalog/datasets/disponibilite-du-parc-nucleaire-d-edf-sa-present-passe-et-previsionnel/records
|
||
|
"""
|
||
|
|
||
|
import json
|
||
|
import requests
|
||
|
import datetime
|
||
|
import logging
|
||
|
import sys
|
||
|
import os
|
||
|
|
||
|
# Add the parent directory to the path so we can import from oedb
|
||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||
|
|
||
|
from oedb.utils.db import db_connect
|
||
|
from oedb.utils.logging import logger
|
||
|
|
||
|
# API URL for EDF open data
|
||
|
API_URL = "https://opendata.edf.fr/api/explore/v2.1/catalog/datasets/disponibilite-du-parc-nucleaire-d-edf-sa-present-passe-et-previsionnel/records"
|
||
|
|
||
|
def fetch_edf_data():
|
||
|
"""
|
||
|
Fetch maintenance planning data from the EDF open data API.
|
||
|
|
||
|
Returns:
|
||
|
list: A list of maintenance events.
|
||
|
"""
|
||
|
logger.info("Fetching data from EDF open data API")
|
||
|
|
||
|
try:
|
||
|
response = requests.get(API_URL)
|
||
|
response.raise_for_status() # Raise an exception for HTTP errors
|
||
|
|
||
|
data = response.json()
|
||
|
|
||
|
if 'results' not in data:
|
||
|
logger.error("No results found in API response")
|
||
|
return []
|
||
|
|
||
|
logger.success(f"Successfully fetched {len(data['results'])} records from EDF API")
|
||
|
return data['results']
|
||
|
|
||
|
except requests.exceptions.RequestException as e:
|
||
|
logger.error(f"Error fetching data from EDF API: {e}")
|
||
|
return []
|
||
|
except json.JSONDecodeError as e:
|
||
|
logger.error(f"Error decoding JSON response: {e}")
|
||
|
return []
|
||
|
|
||
|
def create_event(record):
|
||
|
"""
|
||
|
Create an event object from an EDF record.
|
||
|
|
||
|
Args:
|
||
|
record: A record from the EDF API.
|
||
|
|
||
|
Returns:
|
||
|
dict: A GeoJSON Feature representing the event.
|
||
|
"""
|
||
|
# Extract data from the record
|
||
|
try:
|
||
|
# Extract the nuclear power plant name and unit
|
||
|
site_name = record.get('site', 'Unknown Site')
|
||
|
unit = record.get('unite', 'Unknown Unit')
|
||
|
|
||
|
# Extract start and end dates
|
||
|
start_date = record.get('date_et_heure_fuseau_horaire_europe_paris')
|
||
|
# end_date = record.get('date_fin')
|
||
|
|
||
|
if not start_date or not end_date:
|
||
|
logger.warning(f"Missing start or end date for {site_name} {unit}, skipping")
|
||
|
return None
|
||
|
|
||
|
# Extract power values
|
||
|
power_available = record.get('puissance_disponible')
|
||
|
power_max = record.get('puissance_maximale_possible')
|
||
|
|
||
|
# Extract coordinates (if available)
|
||
|
# Note: The API might not provide coordinates, so we'd need to geocode the site names
|
||
|
# For now, we'll use a placeholder location in France
|
||
|
coordinates = [2.2137, 46.2276] # Center of France
|
||
|
|
||
|
# Create the event object
|
||
|
event = {
|
||
|
"type": "Feature",
|
||
|
"geometry": {
|
||
|
"type": "Point",
|
||
|
"coordinates": coordinates
|
||
|
},
|
||
|
"properties": {
|
||
|
"type": "scheduled",
|
||
|
"what": "energy.maintenance.nuclear",
|
||
|
"what:series": "EDF Nuclear Maintenance",
|
||
|
"where": f"{site_name} - {unit}",
|
||
|
"label": f"Nuclear Maintenance: {site_name} - {unit}",
|
||
|
"start": start_date,
|
||
|
"stop": end_date,
|
||
|
"power_available": power_available,
|
||
|
"power_max": power_max,
|
||
|
"source": "EDF Open Data"
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return event
|
||
|
|
||
|
except Exception as e:
|
||
|
logger.error(f"Error creating event from record: {e}")
|
||
|
return None
|
||
|
|
||
|
def submit_event(event):
|
||
|
"""
|
||
|
Submit an event to the OpenEventDatabase.
|
||
|
|
||
|
Args:
|
||
|
event: A GeoJSON Feature representing the event.
|
||
|
|
||
|
Returns:
|
||
|
bool: True if the event was successfully submitted, False otherwise.
|
||
|
"""
|
||
|
try:
|
||
|
# Connect to the database
|
||
|
db = db_connect()
|
||
|
cur = db.cursor()
|
||
|
|
||
|
# Extract event properties
|
||
|
properties = event['properties']
|
||
|
geometry = json.dumps(event['geometry'])
|
||
|
|
||
|
# Insert the geometry into the geo table
|
||
|
cur.execute("""
|
||
|
INSERT INTO geo
|
||
|
SELECT geom, md5(st_astext(geom)) as hash, st_centroid(geom) as geom_center FROM
|
||
|
(SELECT st_setsrid(st_geomfromgeojson(%s),4326) as geom) as g
|
||
|
WHERE ST_IsValid(geom)
|
||
|
ON CONFLICT DO NOTHING RETURNING hash;
|
||
|
""", (geometry,))
|
||
|
|
||
|
# Get the geometry hash
|
||
|
hash_result = cur.fetchone()
|
||
|
|
||
|
if hash_result is None:
|
||
|
# If the hash is None, get it from the database
|
||
|
cur.execute("""
|
||
|
SELECT md5(st_asewkt(geom)),
|
||
|
ST_IsValid(geom),
|
||
|
ST_IsValidReason(geom) from (SELECT st_geomfromgeojson(%s) as geom) as g;
|
||
|
""", (geometry,))
|
||
|
hash_result = cur.fetchone()
|
||
|
|
||
|
if hash_result is None or (len(hash_result) > 1 and not hash_result[1]):
|
||
|
logger.error(f"Invalid geometry for event: {properties.get('label')}")
|
||
|
db.close()
|
||
|
return False
|
||
|
|
||
|
geo_hash = hash_result[0]
|
||
|
|
||
|
# Determine the bounds for the time range
|
||
|
bounds = '[]' if properties['start'] == properties['stop'] else '[)'
|
||
|
|
||
|
# Insert the event into the database
|
||
|
cur.execute("""
|
||
|
INSERT INTO events (events_type, events_what, events_when, events_tags, events_geo)
|
||
|
VALUES (%s, %s, tstzrange(%s, %s, %s), %s, %s)
|
||
|
ON CONFLICT DO NOTHING RETURNING events_id;
|
||
|
""", (
|
||
|
properties['type'],
|
||
|
properties['what'],
|
||
|
properties['start'],
|
||
|
properties['stop'],
|
||
|
bounds,
|
||
|
json.dumps(properties),
|
||
|
geo_hash
|
||
|
))
|
||
|
|
||
|
# Get the event ID
|
||
|
event_id = cur.fetchone()
|
||
|
|
||
|
if event_id:
|
||
|
logger.success(f"Event created with ID: {event_id[0]}")
|
||
|
db.commit()
|
||
|
db.close()
|
||
|
return True
|
||
|
else:
|
||
|
# Check if the event already exists
|
||
|
cur.execute("""
|
||
|
SELECT events_id FROM events
|
||
|
WHERE events_what = %s
|
||
|
AND events_when = tstzrange(%s, %s, %s)
|
||
|
AND events_geo = %s;
|
||
|
""", (
|
||
|
properties['what'],
|
||
|
properties['start'],
|
||
|
properties['stop'],
|
||
|
bounds,
|
||
|
geo_hash
|
||
|
))
|
||
|
|
||
|
existing_id = cur.fetchone()
|
||
|
|
||
|
if existing_id:
|
||
|
logger.info(f"Event already exists with ID: {existing_id[0]}")
|
||
|
else:
|
||
|
logger.warning(f"Failed to create event: {properties.get('label')}")
|
||
|
|
||
|
db.close()
|
||
|
return False
|
||
|
|
||
|
except Exception as e:
|
||
|
logger.error(f"Error submitting event: {e}")
|
||
|
return False
|
||
|
|
||
|
def main():
|
||
|
"""
|
||
|
Main function to fetch EDF data and add events to the database.
|
||
|
"""
|
||
|
logger.info("Starting EDF schedules extractor")
|
||
|
|
||
|
# Fetch data from the EDF API
|
||
|
records = fetch_edf_data()
|
||
|
|
||
|
if not records:
|
||
|
logger.warning("No records found, exiting")
|
||
|
return
|
||
|
|
||
|
# Process each record
|
||
|
success_count = 0
|
||
|
for record in records:
|
||
|
# Create an event from the record
|
||
|
event = create_event(record)
|
||
|
|
||
|
if not event:
|
||
|
continue
|
||
|
|
||
|
# Submit the event to the database
|
||
|
if submit_event(event):
|
||
|
success_count += 1
|
||
|
|
||
|
logger.success(f"Successfully added {success_count} out of {len(records)} events to the database")
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
main()
|