import pandas as pd import numpy as np import xgboost as xgb import mysql.connector from mysql.connector import Error from datetime import datetime, timedelta import holidays # --- NIEUW: Imports voor grafiek en e-mail --- import matplotlib.pyplot as plt import matplotlib.dates as mdates import smtplib from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.mime.image import MIMEImage # --- NIEUW: Zorgt dat matplotlib werkt zonder GUI (bv. in terminal) --- plt.switch_backend('Agg') # --- CONFIGURATIE --- MODEL_FILE = 'price_forecast_model_v1_5.json' TARGET = 'gemiddelde_prijs' AANTAL_UUR_VOORSPELLEN = 72 GRAFIEK_BESTAND = 'prijs_voorspelling.png' # Tijdelijk bestand # --- NIEUW: E-mail Configuratie --- # VUL DIT IN MET JE EIGEN GEGEVENS EMAIL_CONFIG = { 'smtp_server': '192.168.178.201', # Bv. 'smtp.gmail.com' 'smtp_port': 587, 'sender': 'sftpuser@markkors.nl', 'password': 'Aae8-G9yFU5j', # Voeg hier het wachtwoord toe als dat nodig is 'receiver': 'mark@markkors.nl' } DB_CONFIG = { 'host': '192.168.178.201', 'user': 'energy_prices_user', 'password': 'kS9R*xp17ZwCD@CV&E^N', 'database': 'energy_prices', 'port': 3307 } # Configuratie voor de Alfen database met werkelijke prijzen --- ALFEN_DB_CONFIG = { 'host': '192.168.178.201', 'user': 'alfen_user', 'password': '5uVgr%f%s2P5GR@3q!', 'database': 'alfen', 'port': 3307 } # KIES HIER DE ENERGIELEVERANCIER (of gebruik 'GEMIDDELDE' voor het gemiddelde van alle leveranciers) LEVERANCIER = 'GEMIDDELDE' # Of bijv. 'Eneco', 'Vattenfall', etc. # --- Model laden --- try: print(f"Laden van model: {MODEL_FILE}...") model = xgb.XGBRegressor() model.load_model(MODEL_FILE) FEATURES = model.feature_names_in_ print(f"āœ… Model succesvol geladen (verwacht {len(FEATURES)} features).") except FileNotFoundError: print(f"āŒ Fout: Model bestand '{MODEL_FILE}' niet gevonden.") print(" Heb je het 'v1_5' model al getraind en opgeslagen op Windows?") exit() except Exception as e: print(f"āŒ Fout bij laden model: {e}") exit() nl_holidays = holidays.Netherlands(years=[datetime.now().year, datetime.now().year + 1]) # --- NIEUW: Functie om werkelijke prijzen op te halen uit Alfen database --- def haal_werkelijke_prijzen(start_tijd, eind_tijd): """ Haalt werkelijke stroomprijzen op uit de Alfen database. """ print("\nšŸ’¾ Werkelijke prijzen ophalen uit Alfen database...") try: conn_alfen = mysql.connector.connect(**ALFEN_DB_CONFIG) if LEVERANCIER == 'GEMIDDELDE': # Haal gemiddelde prijs van alle leveranciers query = """ SELECT datetime, AVG(price) as price FROM dynamic_price_data_tommorow WHERE datetime BETWEEN %s AND %s GROUP BY datetime ORDER BY datetime """ else: # Haal specifieke leverancier (voeg eventueel een WHERE clause toe voor leverancier) # Dit hangt af van hoe je leveranciers opslaat in de tabel query = """ SELECT datetime, price FROM dynamic_price_data_tommorow WHERE datetime BETWEEN %s AND %s ORDER BY datetime """ werkelijke_df = pd.read_sql( query, conn_alfen, params=(start_tijd, eind_tijd), index_col='datetime', parse_dates=['datetime'] ) conn_alfen.close() print(f"āœ… {len(werkelijke_df)} werkelijke prijzen opgehaald.") return werkelijke_df except Error as e: print(f"āš ļø Kon geen verbinding maken met Alfen database: {e}") print(" Voorspelling gaat door zonder werkelijke prijzen in grafiek.") return None except Exception as e: print(f"āš ļø Fout bij ophalen werkelijke prijzen: {e}") return None # --- Functie: haal_data_uit_database (onveranderd) --- def haal_data_uit_database(conn): print("šŸ’¾ Data ophalen uit MySQL...") query_hist = """ SELECT w.datum_tijd, w.temperatuur, w.gevoelstemperatuur, w.neerslag, w.wind_richting, w.wind_snelheid, w.bewolking, w.luchtdruk, w.luchtvochtigheid, w.zonnestraling, p_avg.gemiddelde_prijs FROM amersfoort_weer_uurlijks AS w LEFT JOIN (SELECT datetime, AVG(price) AS gemiddelde_prijs FROM dynamic_price_data GROUP BY datetime) AS p_avg ON w.datum_tijd = p_avg.datetime WHERE w.datum_tijd BETWEEN (UTC_TIMESTAMP() - INTERVAL 30 HOUR) AND UTC_TIMESTAMP() ORDER BY w.datum_tijd; """ query_toekomst = f""" SELECT datum_tijd, temperatuur, gevoelstemperatuur, neerslag, wind_richting, wind_snelheid, bewolking, luchtdruk, luchtvochtigheid, zonnestraling, NULL AS gemiddelde_prijs FROM amersfoort_weer_uurlijks WHERE datum_tijd BETWEEN UTC_TIMESTAMP() AND (UTC_TIMESTAMP() + INTERVAL {AANTAL_UUR_VOORSPELLEN} HOUR) ORDER BY datum_tijd; """ try: hist_df = pd.read_sql(query_hist, conn, index_col='datum_tijd', parse_dates=['datum_tijd']) toekomst_df = pd.read_sql(query_toekomst, conn, index_col='datum_tijd', parse_dates=['datum_tijd']) print(f"āœ… {len(hist_df)} uur historie geladen.") print(f"āœ… {len(toekomst_df)} uur toekomstig weer geladen.") hist_df['gemiddelde_prijs'] = hist_df['gemiddelde_prijs'].ffill() hist_df['gemiddelde_prijs'] = hist_df['gemiddelde_prijs'].bfill() combined_df = pd.concat([hist_df, toekomst_df]) return combined_df.sort_index() except Exception as e: print(f"āŒ Fout bij ophalen data: {e}") return None # --- Functie: maak_features_voor_uur (onveranderd) --- def maak_features_voor_uur(df, timestamp): features = {} data_nu = df.loc[timestamp] features['maand'] = timestamp.month features['dag_van_het_jaar'] = timestamp.dayofyear features['is_feestdag'] = 1 if timestamp in nl_holidays else 0 weer_cols = ['temperatuur', 'gevoelstemperatuur', 'neerslag', 'wind_richting', 'wind_snelheid', 'bewolking', 'luchtdruk', 'luchtvochtigheid', 'zonnestraling'] for col in weer_cols: features[col] = data_nu[col] features['prijs_1u_geleden'] = df.loc[timestamp - timedelta(hours=1)]['gemiddelde_prijs'] features['prijs_24u_geleden'] = df.loc[timestamp - timedelta(hours=24)]['gemiddelde_prijs'] features['temp_avg_3u'] = df.loc[timestamp - timedelta(hours=2) : timestamp]['temperatuur'].mean() features['prijs_avg_6u'] = df.loc[timestamp - timedelta(hours=5) : timestamp]['gemiddelde_prijs'].mean() for dag in range(7): features[f'dag_{dag}'] = 1 if timestamp.dayofweek == dag else 0 for uur in range(24): features[f'uur_{uur}'] = 1 if timestamp.hour == uur else 0 return pd.DataFrame([features], columns=FEATURES) # --- NIEUW: Functie om de e-mail te bouwen en te versturen --- def send_email_with_graph(image_path, result_df): print("\nšŸ“¬ E-mail opstellen...") msg = MIMEMultipart() msg['Subject'] = f"Energie Prijsvoorspelling [energy_prediction] {datetime.now().strftime('%d-%m-%Y')}" msg['From'] = EMAIL_CONFIG['sender'] msg['To'] = EMAIL_CONFIG['receiver'] laagste_prijs = result_df['Voorspelde_Prijs'].min() hoogste_prijs = result_df['Voorspelde_Prijs'].max() laagste_moment = result_df['Voorspelde_Prijs'].idxmin() hoogste_moment = result_df['Voorspelde_Prijs'].idxmax() body = f""" Prijsvoorspelling voor de komende {len(result_df)} uur. Laagste prijs: {laagste_prijs:.4f} (op {laagste_moment}) Hoogste prijs: {hoogste_prijs:.4f} (op {hoogste_moment}) Volledige voorspelling: {result_df.to_string()} """ msg.attach(MIMEText(body, 'plain')) try: with open(image_path, 'rb') as f: img_attach = MIMEImage(f.read(), name='voorspelling.png') msg.attach(img_attach) print("āœ… Grafiek bijgevoegd.") except Exception as e: print(f"āŒ Kon grafiek-bestand niet lezen: {e}") return try: print(f"Verbinding maken met SMTP server: {EMAIL_CONFIG['smtp_server']}...") server = smtplib.SMTP(EMAIL_CONFIG['smtp_server'], EMAIL_CONFIG['smtp_port']) server.starttls() server.login(EMAIL_CONFIG['sender'], EMAIL_CONFIG['password']) print("Inloggen succesvol. E-mail verzenden...") server.sendmail(EMAIL_CONFIG['sender'], EMAIL_CONFIG['receiver'], msg.as_string()) server.quit() print("āœ… E-mail succesvol verzonden.") except Exception as e: print(f"āŒ Fout bij verzenden e-mail: {e}") print(" Controleer je EMAIL_CONFIG (server, poort, e-mail en App-wachtwoord)") # --- START VAN HET SCRIPT (Aangepast) --- try: conn = mysql.connector.connect(**DB_CONFIG) werk_df = haal_data_uit_database(conn) if werk_df is not None: te_voorspellen_tijden = werk_df[werk_df['gemiddelde_prijs'].isnull()].index if len(te_voorspellen_tijden) == 0: print("\nDatabase is al up-to-date. Geen voorspelling nodig.") else: print(f"\n🧠 Start iteratieve voorspelling voor {len(te_voorspellen_tijden)} uur...") voorspellingen = [] for timestamp in te_voorspellen_tijden: features_nu = maak_features_voor_uur(werk_df, timestamp) voorspelde_prijs = model.predict(features_nu)[0] werk_df.loc[timestamp, 'gemiddelde_prijs'] = voorspelde_prijs voorspellingen.append(voorspelde_prijs) print("\n" + "="*70) pd.set_option('display.max_rows', None) print(f"--- VOORSPELDE PRIJZEN (komende {len(te_voorspellen_tijden)} uur) ---") resultaat_df = pd.DataFrame({ 'Voorspelde_Prijs': voorspellingen }, index=te_voorspellen_tijden) print(resultaat_df) print("="*70) # --- NIEUW: Haal werkelijke prijzen op --- start_tijd = te_voorspellen_tijden[0] eind_tijd = te_voorspellen_tijden[-1] werkelijke_prijzen_df = haal_werkelijke_prijzen(start_tijd, eind_tijd) # Grafiek maken (met werkelijke prijzen) --- print("\nšŸ“Š Grafiek genereren (met werkelijke prijzen en weer)...") try: # Maak de basis-figuur en de EERSTE Y-as (ax1) voor de prijs fig, ax1 = plt.subplots(figsize=(15, 8)) # Plot de Voorspelde Prijs op de linker-as (ax1) ax1.plot( resultaat_df.index, resultaat_df['Voorspelde_Prijs'], color='blue', marker='.', linestyle='-', linewidth=2, label='Voorspelde Prijs' ) # --- NIEUW: Plot werkelijke prijzen als die beschikbaar zijn --- if werkelijke_prijzen_df is not None and not werkelijke_prijzen_df.empty: ax1.plot( werkelijke_prijzen_df.index, werkelijke_prijzen_df['price'], color='orange', marker='o', linestyle='--', linewidth=2, label=f'Werkelijke Prijs ({LEVERANCIER})' ) print(f"āœ… Werkelijke prijzen toegevoegd aan grafiek ({len(werkelijke_prijzen_df)} punten).") ax1.set_ylabel('Prijs (€)', color='blue', fontsize=12) ax1.tick_params(axis='y', labelcolor='blue') ax1.set_xlabel('Datum en Tijd', fontsize=12) ax1.grid(True, which='major', axis='x', alpha=0.3) # Maak de TWEEDE Y-as (ax2) die de X-as deelt ax2 = ax1.twinx() weer_toekomst = werk_df.loc[te_voorspellen_tijden] # Plot Temperatuur op de rechter-as (ax2) ax2.plot( weer_toekomst.index, weer_toekomst['temperatuur'], color='red', marker='.', linestyle='--', alpha=0.6, label='Temperatuur (°C)' ) # Plot Windsnelheid op de rechter-as (ax2) ax2.plot( weer_toekomst.index, weer_toekomst['wind_snelheid'], color='green', marker='x', linestyle=':', alpha=0.6, label='Windsnelheid (km/u)' ) ax2.set_ylabel('Temperatuur / Windsnelheid', color='black', fontsize=12) ax2.tick_params(axis='y', labelcolor='black') # Titel en gecombineerde legenda plt.title(f'Energieprijs Voorspelling vs Werkelijk ({len(resultaat_df)} uur)', fontsize=16, fontweight='bold') lines1, labels1 = ax1.get_legend_handles_labels() lines2, labels2 = ax2.get_legend_handles_labels() ax2.legend(lines1 + lines2, labels1 + labels2, loc='upper left', fontsize=10) # X-as opmaken (Nederlands formaat) date_format = mdates.DateFormatter('%d-%m %H:%M') ax1.xaxis.set_major_formatter(date_format) plt.setp(ax1.get_xticklabels(), rotation=30, ha='right') fig.tight_layout() # Sla de grafiek op plt.savefig(GRAFIEK_BESTAND, dpi=150) plt.close(fig) print(f"āœ… Grafiek opgeslagen als: {GRAFIEK_BESTAND}") # Stuur de e-mail send_email_with_graph(GRAFIEK_BESTAND, resultaat_df) except Exception as e: print(f"āŒ Fout bij genereren van grafiek: {e}") import traceback traceback.print_exc() except Error as e: print(f"āŒ Fout met MySQL verbinding: {e}") except Exception as e: print(f"āŒ Een onverwachte fout is opgetreden: {e}") import traceback traceback.print_exc() finally: if 'conn' in locals() and conn.is_connected(): conn.close() print("\nVerbinding met MySQL gesloten.")