diff --git a/README-DE.md b/README-DE.md index bbe4ba5..482c7fb 100644 --- a/README-DE.md +++ b/README-DE.md @@ -8,7 +8,7 @@ Die Diskussion findet im [Forum](https://www.akkudoktor.net/forum/diy-energie-op ## Installation -Gute Install Anleitung: +Gute Install Anleitung: https://meintechblog.de/2024/09/05/andreas-schmitz-joerg-installiert-mein-energieoptimierungssystem/ Das Projekt erfordert Python 3.8 oder neuer. @@ -115,7 +115,7 @@ Beschreibt die Struktur und Datentypen des JSON-Objekts, das an den Flask-Server - **Beschreibung**: Ein Array von Floats, das den Strompreis in Euro pro Wattstunde für verschiedene Zeitintervalle darstellt. - **Typ**: Array - **Element-Typ**: Float -- **Länge**: 48 +- **Länge**: 48 ### gesamtlast - **Beschreibung**: Ein Array von Floats, das die Gesamtlast (Verbrauch) in Watt für verschiedene Zeitintervalle darstellt. @@ -136,7 +136,7 @@ Beschreibt die Struktur und Datentypen des JSON-Objekts, das an den Flask-Server - **Länge**: 48 ### pv_soc -- **Beschreibung**: Ein Integer, der den Ladezustand des PV Akkus zum START der aktuellen Stunde anzeigt, das ist nicht der aktuelle!!! +- **Beschreibung**: Ein Integer, der den Ladezustand des PV Akkus zum START der aktuellen Stunde anzeigt, das ist nicht der aktuelle!!! - **Typ**: Integer ### pv_akku_cap diff --git a/README.md b/README.md index c03bd9e..76cbd53 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The discussion takes place in the [forum](https://www.akkudoktor.net/forum/diy-e ## Installation -Good installation guide: +Good installation guide: https://meintechblog.de/2024/09/05/andreas-schmitz-joerg-installiert-mein-energieoptimierungssystem/ The project requires Python 3.8 or newer. @@ -110,7 +110,7 @@ Describes the structure and data types of the JSON object sent to the Flask serv - **Description**: An array of floats representing the electricity price in euros per watt-hour for different time intervals. - **Type**: Array - **Element Type**: Float -- **Length**: 48 +- **Length**: 48 ### `gesamtlast` - **Description**: An array of floats representing the total load (consumption) in watts for different time intervals. @@ -214,4 +214,4 @@ This object contains the results of the simulation and provides insights into va - **Netzbezug_Wh_pro_Stunde**: The grid energy drawn in watt-hours per hour. - **Netzeinspeisung_Wh_pro_Stunde**: The energy fed into the grid in watt-hours per hour. - **Verluste_Pro_Stunde**: The losses in watt-hours per hour. -- **akku_soc_pro_stunde**: The state of charge of the battery (not the EV) in percentage per hour. \ No newline at end of file +- **akku_soc_pro_stunde**: The state of charge of the battery (not the EV) in percentage per hour. diff --git a/config.example.py b/config.example.py index 06a9034..c102016 100644 --- a/config.example.py +++ b/config.example.py @@ -1,28 +1,38 @@ from datetime import datetime, timedelta - -prediction_hours=48 -optimization_hours=24 -strafe=10 -moegliche_ladestroeme_in_prozent = [0.0 ,6.0/16.0, 7.0/16.0, 8.0/16.0, 9.0/16.0, 10.0/16.0, 11.0/16.0, 12.0/16.0, 13.0/16.0, 14.0/16.0, 15.0/16.0, 1.0 ] +prediction_hours = 48 +optimization_hours = 24 +strafe = 10 +moegliche_ladestroeme_in_prozent = [ + 0.0, + 6.0 / 16.0, + 7.0 / 16.0, + 8.0 / 16.0, + 9.0 / 16.0, + 10.0 / 16.0, + 11.0 / 16.0, + 12.0 / 16.0, + 13.0 / 16.0, + 14.0 / 16.0, + 15.0 / 16.0, + 1.0, +] # Optional -db_config = { - 'user': 'eos', - 'password': 'eos', - 'host': '127.0.0.1', - 'database': 'eos' -} +db_config = {"user": "eos", "password": "eos", "host": "127.0.0.1", "database": "eos"} + -def get_start_enddate(prediction_hours=48,startdate=None): - ############ - # Parameter - ############ - if startdate == None: - date = (datetime.now().date() + timedelta(hours = prediction_hours)).strftime("%Y-%m-%d") - date_now = datetime.now().strftime("%Y-%m-%d") - else: - date = (startdate + timedelta(hours = prediction_hours)).strftime("%Y-%m-%d") - date_now = startdate.strftime("%Y-%m-%d") - return date_now,date +def get_start_enddate(prediction_hours=48, startdate=None): + ############ + # Parameter + ############ + if startdate == None: + date = (datetime.now().date() + timedelta(hours=prediction_hours)).strftime( + "%Y-%m-%d" + ) + date_now = datetime.now().strftime("%Y-%m-%d") + else: + date = (startdate + timedelta(hours=prediction_hours)).strftime("%Y-%m-%d") + date_now = startdate.strftime("%Y-%m-%d") + return date_now, date diff --git a/flask_server.py b/flask_server.py index 7f152be..69f23e3 100755 --- a/flask_server.py +++ b/flask_server.py @@ -1,19 +1,18 @@ #!/usr/bin/env python3 import os -import random - -from pprint import pprint import matplotlib -matplotlib.use('Agg') # Sets the Matplotlib backend to 'Agg' for rendering plots in environments without a display -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd -from deap import base, creator, tools, algorithms -from flask import Flask, jsonify, request, redirect, send_from_directory, url_for +matplotlib.use( + "Agg" +) # Sets the Matplotlib backend to 'Agg' for rendering plots in environments without a display +from datetime import datetime, timedelta + +import pandas as pd from config import * +from flask import Flask, jsonify, redirect, request, send_from_directory, url_for + from modules.class_akku import * from modules.class_ems import * from modules.class_heatpump import * @@ -26,10 +25,12 @@ from modules.class_sommerzeit import * from modules.class_strompreis import * from modules.visualize import * -from datetime import datetime, timedelta + app = Flask(__name__) -opt_class = optimization_problem(prediction_hours=prediction_hours, strafe=10, optimization_hours=optimization_hours) +opt_class = optimization_problem( + prediction_hours=prediction_hours, strafe=10, optimization_hours=optimization_hours +) # @app.route('/last_correction', methods=['GET']) # def flask_last_correction(): @@ -53,7 +54,8 @@ # print(last) # return jsonify(last.tolist()) -@app.route('/soc', methods=['GET']) + +@app.route("/soc", methods=["GET"]) def flask_soc(): # MariaDB connection details config = db_config @@ -66,15 +68,24 @@ def flask_soc(): bat_capacity = 33 * 1000 / 48 # Battery capacity in watt-hours # Define the reference time point (3 weeks ago) - zeitpunkt_x = (datetime.now() - timedelta(weeks=3)).strftime('%Y-%m-%d %H:%M:%S') + zeitpunkt_x = (datetime.now() - timedelta(weeks=3)).strftime("%Y-%m-%d %H:%M:%S") # Instantiate BatteryDataProcessor and perform calculations - processor = BatteryDataProcessor(config, voltage_high_threshold, voltage_low_threshold, current_low_threshold, gap, bat_capacity) + processor = BatteryDataProcessor( + config, + voltage_high_threshold, + voltage_low_threshold, + current_low_threshold, + gap, + bat_capacity, + ) processor.connect_db() processor.fetch_data(zeitpunkt_x) processor.process_data() last_points_100_df, last_points_0_df = processor.find_soc_points() - soc_df, integration_results = processor.calculate_resetting_soc(last_points_100_df, last_points_0_df) + soc_df, integration_results = processor.calculate_resetting_soc( + last_points_100_df, last_points_0_df + ) # soh_df = processor.calculate_soh(integration_results) # Optional State of Health calculation processor.update_database_with_soc(soc_df) # Update database with SOC data # processor.plot_data(last_points_100_df, last_points_0_df, soc_df) # Optional data visualization @@ -82,50 +93,65 @@ def flask_soc(): return jsonify("Done") -@app.route('/strompreis', methods=['GET']) + +@app.route("/strompreis", methods=["GET"]) def flask_strompreis(): # Get the current date and the end date based on prediction hours - date_now, date = get_start_enddate(prediction_hours, startdate=datetime.now().date()) - filepath = os.path.join(r'test_data', r'strompreise_akkudokAPI.json') # Adjust the path to the JSON file - price_forecast = HourlyElectricityPriceForecast(source=f"https://api.akkudoktor.net/prices?start={date_now}&end={date}", prediction_hours=prediction_hours) - specific_date_prices = price_forecast.get_price_for_daterange(date_now, date) # Fetch prices for the specified date range + date_now, date = get_start_enddate( + prediction_hours, startdate=datetime.now().date() + ) + filepath = os.path.join( + r"test_data", r"strompreise_akkudokAPI.json" + ) # Adjust the path to the JSON file + price_forecast = HourlyElectricityPriceForecast( + source=f"https://api.akkudoktor.net/prices?start={date_now}&end={date}", + prediction_hours=prediction_hours, + ) + specific_date_prices = price_forecast.get_price_for_daterange( + date_now, date + ) # Fetch prices for the specified date range return jsonify(specific_date_prices.tolist()) + # Endpoint to handle total load calculation based on the latest measured data -@app.route('/gesamtlast', methods=['POST']) +@app.route("/gesamtlast", methods=["POST"]) def flask_gesamtlast(): # Retrieve data from the JSON body data = request.get_json() # Extract year_energy and prediction_hours from the request JSON year_energy = float(data.get("year_energy")) - prediction_hours = int(data.get("hours", 48)) # Default to 48 hours if not specified + prediction_hours = int( + data.get("hours", 48) + ) # Default to 48 hours if not specified # Measured data in JSON format measured_data_json = data.get("measured_data") measured_data = pd.DataFrame(measured_data_json) - measured_data['time'] = pd.to_datetime(measured_data['time']) + measured_data["time"] = pd.to_datetime(measured_data["time"]) # Ensure datetime has timezone info for accurate calculations - if measured_data['time'].dt.tz is None: - measured_data['time'] = measured_data['time'].dt.tz_localize('Europe/Berlin') + if measured_data["time"].dt.tz is None: + measured_data["time"] = measured_data["time"].dt.tz_localize("Europe/Berlin") else: - measured_data['time'] = measured_data['time'].dt.tz_convert('Europe/Berlin') + measured_data["time"] = measured_data["time"].dt.tz_convert("Europe/Berlin") # Remove timezone info after conversion to simplify further processing - measured_data['time'] = measured_data['time'].dt.tz_localize(None) + measured_data["time"] = measured_data["time"].dt.tz_localize(None) # Instantiate LoadForecast and generate forecast data - lf = LoadForecast(filepath=r'load_profiles.npz', year_energy=year_energy) + lf = LoadForecast(filepath=r"load_profiles.npz", year_energy=year_energy) forecast_list = [] - + # Generate daily forecasts for the date range based on measured data - for single_date in pd.date_range(measured_data['time'].min().date(), measured_data['time'].max().date()): - date_str = single_date.strftime('%Y-%m-%d') + for single_date in pd.date_range( + measured_data["time"].min().date(), measured_data["time"].max().date() + ): + date_str = single_date.strftime("%Y-%m-%d") daily_forecast = lf.get_daily_stats(date_str) mean_values = daily_forecast[0] hours = [single_date + pd.Timedelta(hours=i) for i in range(24)] - daily_forecast_df = pd.DataFrame({'time': hours, 'Last Pred': mean_values}) + daily_forecast_df = pd.DataFrame({"time": hours, "Last Pred": mean_values}) forecast_list.append(daily_forecast_df) # Concatenate all daily forecasts into a single DataFrame @@ -135,17 +161,22 @@ def flask_gesamtlast(): adjuster = LoadPredictionAdjuster(measured_data, predicted_data, lf) adjuster.calculate_weighted_mean() # Calculate weighted mean for adjustment adjuster.adjust_predictions() # Adjust predictions based on measured data - future_predictions = adjuster.predict_next_hours(prediction_hours) # Predict future load + future_predictions = adjuster.predict_next_hours( + prediction_hours + ) # Predict future load # Extract household power predictions - leistung_haushalt = future_predictions['Adjusted Pred'].values + leistung_haushalt = future_predictions["Adjusted Pred"].values gesamtlast = Gesamtlast(prediction_hours=prediction_hours) - gesamtlast.hinzufuegen("Haushalt", leistung_haushalt) # Add household load to total load calculation + gesamtlast.hinzufuegen( + "Haushalt", leistung_haushalt + ) # Add household load to total load calculation # Calculate the total load last = gesamtlast.gesamtlast_berechnen() # Compute total load return jsonify(last.tolist()) + # @app.route('/gesamtlast', methods=['GET']) # def flask_gesamtlast(): # if request.method == 'GET': @@ -207,20 +238,33 @@ def flask_gesamtlast(): # print(last) # Output total load # return jsonify(last.tolist()) # Return total load as JSON -@app.route('/gesamtlast_simple', methods=['GET']) + +@app.route("/gesamtlast_simple", methods=["GET"]) def flask_gesamtlast_simple(): - if request.method == 'GET': - year_energy = float(request.args.get("year_energy")) # Get annual energy value from query parameters - date_now, date = get_start_enddate(prediction_hours, startdate=datetime.now().date()) # Get the current date and prediction end date + if request.method == "GET": + year_energy = float( + request.args.get("year_energy") + ) # Get annual energy value from query parameters + date_now, date = get_start_enddate( + prediction_hours, startdate=datetime.now().date() + ) # Get the current date and prediction end date ############### # Load Forecast ############### - lf = LoadForecast(filepath=r'load_profiles.npz', year_energy=year_energy) # Instantiate LoadForecast with specified parameters - leistung_haushalt = lf.get_stats_for_date_range(date_now, date)[0] # Get expected household load for the date range - - gesamtlast = Gesamtlast(prediction_hours=prediction_hours) # Create Gesamtlast instance - gesamtlast.hinzufuegen("Haushalt", leistung_haushalt) # Add household load to total load calculation + lf = LoadForecast( + filepath=r"load_profiles.npz", year_energy=year_energy + ) # Instantiate LoadForecast with specified parameters + leistung_haushalt = lf.get_stats_for_date_range(date_now, date)[ + 0 + ] # Get expected household load for the date range + + gesamtlast = Gesamtlast( + prediction_hours=prediction_hours + ) # Create Gesamtlast instance + gesamtlast.hinzufuegen( + "Haushalt", leistung_haushalt + ) # Add household load to total load calculation # ############### # # WP (Heat Pump) @@ -233,56 +277,91 @@ def flask_gesamtlast_simple(): return jsonify(last.tolist()) # Return total load as JSON -@app.route('/pvforecast', methods=['GET']) +@app.route("/pvforecast", methods=["GET"]) def flask_pvprognose(): - if request.method == 'GET': + if request.method == "GET": # Retrieve URL and AC power measurement from query parameters url = request.args.get("url") ac_power_measurement = request.args.get("ac_power_measurement") - date_now, date = get_start_enddate(prediction_hours, startdate=datetime.now().date()) + date_now, date = get_start_enddate( + prediction_hours, startdate=datetime.now().date() + ) ############### # PV Forecast ############### - PVforecast = PVForecast(prediction_hours=prediction_hours, url=url) # Instantiate PVForecast with given parameters - if isfloat(ac_power_measurement): # Check if the AC power measurement is a valid float - PVforecast.update_ac_power_measurement(date_time=datetime.now(), ac_power_measurement=float(ac_power_measurement)) # Update measurement - + PVforecast = PVForecast( + prediction_hours=prediction_hours, url=url + ) # Instantiate PVForecast with given parameters + if isfloat( + ac_power_measurement + ): # Check if the AC power measurement is a valid float + PVforecast.update_ac_power_measurement( + date_time=datetime.now(), + ac_power_measurement=float(ac_power_measurement), + ) # Update measurement + # Get PV forecast and temperature forecast for the specified date range pv_forecast = PVforecast.get_pv_forecast_for_date_range(date_now, date) temperature_forecast = PVforecast.get_temperature_for_date_range(date_now, date) # Return both forecasts as a JSON response - ret = {"temperature": temperature_forecast.tolist(), "pvpower": pv_forecast.tolist()} + ret = { + "temperature": temperature_forecast.tolist(), + "pvpower": pv_forecast.tolist(), + } return jsonify(ret) -@app.route('/optimize', methods=['POST']) + +@app.route("/optimize", methods=["POST"]) def flask_optimize(): - if request.method == 'POST': + if request.method == "POST": from datetime import datetime + # Retrieve optimization parameters from the request JSON parameter = request.json # Check for required parameters - required_parameters = ['preis_euro_pro_wh_akku', 'strompreis_euro_pro_wh', "gesamtlast", 'pv_akku_cap', - "einspeiseverguetung_euro_pro_wh", 'pv_forecast', 'temperature_forecast', - 'eauto_min_soc', "eauto_cap", "eauto_charge_efficiency", "eauto_charge_power", - "eauto_soc", "pv_soc", "start_solution", "haushaltsgeraet_dauer", - "haushaltsgeraet_wh"] + required_parameters = [ + "preis_euro_pro_wh_akku", + "strompreis_euro_pro_wh", + "gesamtlast", + "pv_akku_cap", + "einspeiseverguetung_euro_pro_wh", + "pv_forecast", + "temperature_forecast", + "eauto_min_soc", + "eauto_cap", + "eauto_charge_efficiency", + "eauto_charge_power", + "eauto_soc", + "pv_soc", + "start_solution", + "haushaltsgeraet_dauer", + "haushaltsgeraet_wh", + ] # Identify any missing parameters missing_params = [p for p in required_parameters if p not in parameter] if missing_params: - return jsonify({"error": f"Missing parameter: {', '.join(missing_params)}"}), 400 # Return error for missing parameters + return jsonify( + {"error": f"Missing parameter: {', '.join(missing_params)}"} + ), 400 # Return error for missing parameters # Perform optimization simulation - result = opt_class.optimierung_ems(parameter=parameter, start_hour=datetime.now().hour) + result = opt_class.optimierung_ems( + parameter=parameter, start_hour=datetime.now().hour + ) return jsonify(result) # Return optimization results as JSON -@app.route('/visualisierungsergebnisse.pdf') + +@app.route("/visualisierungsergebnisse.pdf") def get_pdf(): # Endpoint to serve the generated PDF with visualization results - return send_from_directory('', 'visualisierungsergebnisse.pdf') # Adjust the directory if needed + return send_from_directory( + "", "visualisierungsergebnisse.pdf" + ) # Adjust the directory if needed + @app.route("/site-map") def site_map(): @@ -299,28 +378,32 @@ def has_no_empty_params(rule): defaults = rule.defaults if rule.defaults is not None else () arguments = rule.arguments if rule.arguments is not None else () return len(defaults) >= len(arguments) + # Collect all valid GET routes without empty parameters links = [] for rule in app.url_map.iter_rules(): if "GET" in rule.methods and has_no_empty_params(rule): url = url_for(rule.endpoint, **(rule.defaults or {})) links.append(url) - return print_links(sorted(links))# Return the sorted links as HTML + return print_links(sorted(links)) # Return the sorted links as HTML -@app.route('/') +@app.route("/") def root(): # Redirect the root URL to the site map return redirect("/site-map", code=302) -if __name__ == '__main__': + +if __name__ == "__main__": try: # Set host and port from environment variables or defaults host = os.getenv("FLASK_RUN_HOST", "0.0.0.0") port = os.getenv("FLASK_RUN_PORT", 8503) app.run(debug=True, host=host, port=port) # Run the Flask application except Exception as e: - print(f"Could not bind to host {host}:{port}. Error: {e}") # Error handling for binding issues + print( + f"Could not bind to host {host}:{port}. Error: {e}" + ) # Error handling for binding issues # PV Forecast: # object { diff --git a/modules/class_akku.py b/modules/class_akku.py index 17f1477..6b840cb 100644 --- a/modules/class_akku.py +++ b/modules/class_akku.py @@ -1,27 +1,40 @@ import numpy as np + class PVAkku: - def __init__(self, kapazitaet_wh=None, hours=None, lade_effizienz=0.88, entlade_effizienz=0.88, - max_ladeleistung_w=None, start_soc_prozent=0, min_soc_prozent=0, max_soc_prozent=100): + def __init__( + self, + kapazitaet_wh=None, + hours=None, + lade_effizienz=0.88, + entlade_effizienz=0.88, + max_ladeleistung_w=None, + start_soc_prozent=0, + min_soc_prozent=0, + max_soc_prozent=100, + ): # Battery capacity in Wh self.kapazitaet_wh = kapazitaet_wh # Initial state of charge in Wh self.start_soc_prozent = start_soc_prozent self.soc_wh = (start_soc_prozent / 100) * kapazitaet_wh - self.hours = hours if hours is not None else 24 # Default to 24 hours if not specified + self.hours = ( + hours if hours is not None else 24 + ) # Default to 24 hours if not specified self.discharge_array = np.full(self.hours, 1) self.charge_array = np.full(self.hours, 1) # Charge and discharge efficiency self.lade_effizienz = lade_effizienz - self.entlade_effizienz = entlade_effizienz - self.max_ladeleistung_w = max_ladeleistung_w if max_ladeleistung_w else self.kapazitaet_wh + self.entlade_effizienz = entlade_effizienz + self.max_ladeleistung_w = ( + max_ladeleistung_w if max_ladeleistung_w else self.kapazitaet_wh + ) self.min_soc_prozent = min_soc_prozent self.max_soc_prozent = max_soc_prozent # Calculate min and max SoC in Wh self.min_soc_wh = (self.min_soc_prozent / 100) * self.kapazitaet_wh self.max_soc_wh = (self.max_soc_prozent / 100) * self.kapazitaet_wh - def to_dict(self): return { "kapazitaet_wh": self.kapazitaet_wh, @@ -32,7 +45,7 @@ def to_dict(self): "charge_array": self.charge_array.tolist(), "lade_effizienz": self.lade_effizienz, "entlade_effizienz": self.entlade_effizienz, - "max_ladeleistung_w": self.max_ladeleistung_w + "max_ladeleistung_w": self.max_ladeleistung_w, } @classmethod @@ -44,20 +57,22 @@ def from_dict(cls, data): lade_effizienz=data["lade_effizienz"], entlade_effizienz=data["entlade_effizienz"], max_ladeleistung_w=data["max_ladeleistung_w"], - start_soc_prozent=data["start_soc_prozent"] + start_soc_prozent=data["start_soc_prozent"], ) # Set arrays obj.discharge_array = np.array(data["discharge_array"]) obj.charge_array = np.array(data["charge_array"]) - obj.soc_wh = data["soc_wh"] # Set current state of charge, which may differ from start_soc_prozent - + obj.soc_wh = data[ + "soc_wh" + ] # Set current state of charge, which may differ from start_soc_prozent + return obj def reset(self): self.soc_wh = (self.start_soc_prozent / 100) * self.kapazitaet_wh # Ensure soc_wh is within min and max limits self.soc_wh = min(max(self.soc_wh, self.min_soc_wh), self.max_soc_wh) - + self.discharge_array = np.full(self.hours, 1) self.charge_array = np.full(self.hours, 1) @@ -77,8 +92,12 @@ def energie_abgeben(self, wh, hour): return 0.0, 0.0 # No energy discharge and no losses # Calculate the maximum energy that can be discharged considering min_soc and efficiency - max_possible_discharge_wh = (self.soc_wh - self.min_soc_wh) * self.entlade_effizienz - max_possible_discharge_wh = max(max_possible_discharge_wh, 0.0) # Ensure non-negative + max_possible_discharge_wh = ( + self.soc_wh - self.min_soc_wh + ) * self.entlade_effizienz + max_possible_discharge_wh = max( + max_possible_discharge_wh, 0.0 + ) # Ensure non-negative # Consider the maximum discharge power of the battery max_abgebbar_wh = min(max_possible_discharge_wh, self.max_ladeleistung_w) @@ -88,7 +107,9 @@ def energie_abgeben(self, wh, hour): # Calculate the actual amount withdrawn from the battery (before efficiency loss) if self.entlade_effizienz > 0: - tatsaechliche_entnahme_wh = tatsaechlich_abgegeben_wh / self.entlade_effizienz + tatsaechliche_entnahme_wh = ( + tatsaechlich_abgegeben_wh / self.entlade_effizienz + ) else: tatsaechliche_entnahme_wh = 0.0 @@ -103,7 +124,6 @@ def energie_abgeben(self, wh, hour): # Return the actually discharged energy and the losses return tatsaechlich_abgegeben_wh, verluste_wh - def energie_laden(self, wh, hour): if hour is not None and self.charge_array[hour] == 0: return 0, 0 # Charging not allowed in this hour @@ -117,7 +137,9 @@ def energie_laden(self, wh, hour): # Calculate the maximum energy that can be charged considering max_soc and efficiency if self.lade_effizienz > 0: - max_possible_charge_wh = (self.max_soc_wh - self.soc_wh) / self.lade_effizienz + max_possible_charge_wh = ( + self.max_soc_wh - self.soc_wh + ) / self.lade_effizienz else: max_possible_charge_wh = 0.0 max_possible_charge_wh = max(max_possible_charge_wh, 0.0) # Ensure non-negative @@ -138,7 +160,6 @@ def energie_laden(self, wh, hour): return geladene_menge, verluste_wh - def aktueller_energieinhalt(self): """ This method returns the current remaining energy considering efficiency. @@ -149,11 +170,16 @@ def aktueller_energieinhalt(self): return max(nutzbare_energie, 0.0) - -if __name__ == '__main__': +if __name__ == "__main__": # Test battery discharge below min_soc print("Test: Discharge below min_soc") - akku = PVAkku(kapazitaet_wh=10000, hours=1, start_soc_prozent=50, min_soc_prozent=20, max_soc_prozent=80) + akku = PVAkku( + kapazitaet_wh=10000, + hours=1, + start_soc_prozent=50, + min_soc_prozent=20, + max_soc_prozent=80, + ) akku.reset() print(f"Initial SoC: {akku.ladezustand_in_prozent()}%") @@ -165,7 +191,13 @@ def aktueller_energieinhalt(self): # Test battery charge above max_soc print("\nTest: Charge above max_soc") - akku = PVAkku(kapazitaet_wh=10000, hours=1, start_soc_prozent=50, min_soc_prozent=20, max_soc_prozent=80) + akku = PVAkku( + kapazitaet_wh=10000, + hours=1, + start_soc_prozent=50, + min_soc_prozent=20, + max_soc_prozent=80, + ) akku.reset() print(f"Initial SoC: {akku.ladezustand_in_prozent()}%") @@ -177,7 +209,13 @@ def aktueller_energieinhalt(self): # Test charging when battery is at max_soc print("\nTest: Charging when at max_soc") - akku = PVAkku(kapazitaet_wh=10000, hours=1, start_soc_prozent=80, min_soc_prozent=20, max_soc_prozent=80) + akku = PVAkku( + kapazitaet_wh=10000, + hours=1, + start_soc_prozent=80, + min_soc_prozent=20, + max_soc_prozent=80, + ) akku.reset() print(f"Initial SoC: {akku.ladezustand_in_prozent()}%") @@ -187,7 +225,13 @@ def aktueller_energieinhalt(self): # Test discharging when battery is at min_soc print("\nTest: Discharging when at min_soc") - akku = PVAkku(kapazitaet_wh=10000, hours=1, start_soc_prozent=20, min_soc_prozent=20, max_soc_prozent=80) + akku = PVAkku( + kapazitaet_wh=10000, + hours=1, + start_soc_prozent=20, + min_soc_prozent=20, + max_soc_prozent=80, + ) akku.reset() print(f"Initial SoC: {akku.ladezustand_in_prozent()}%") diff --git a/modules/class_ems.py b/modules/class_ems.py index 5eee9a5..74f603e 100644 --- a/modules/class_ems.py +++ b/modules/class_ems.py @@ -1,7 +1,7 @@ from datetime import datetime -from pprint import pprint + import numpy as np -import modules.class_akku as PVAkku + def replace_nan_with_none(data): if isinstance(data, dict): @@ -18,31 +18,40 @@ def replace_nan_with_none(data): return data - - class EnergieManagementSystem: - def __init__(self, pv_prognose_wh=None, strompreis_euro_pro_wh=None, einspeiseverguetung_euro_pro_wh=None, eauto=None, gesamtlast=None, haushaltsgeraet=None, wechselrichter=None): + def __init__( + self, + pv_prognose_wh=None, + strompreis_euro_pro_wh=None, + einspeiseverguetung_euro_pro_wh=None, + eauto=None, + gesamtlast=None, + haushaltsgeraet=None, + wechselrichter=None, + ): self.akku = wechselrichter.akku - #self.lastkurve_wh = lastkurve_wh + # self.lastkurve_wh = lastkurve_wh self.gesamtlast = gesamtlast self.pv_prognose_wh = pv_prognose_wh - self.strompreis_euro_pro_wh = strompreis_euro_pro_wh # Strompreis in Cent pro Wh - self.einspeiseverguetung_euro_pro_wh = einspeiseverguetung_euro_pro_wh # Einspeisevergütung in Cent pro Wh + self.strompreis_euro_pro_wh = ( + strompreis_euro_pro_wh # Strompreis in Cent pro Wh + ) + self.einspeiseverguetung_euro_pro_wh = ( + einspeiseverguetung_euro_pro_wh # Einspeisevergütung in Cent pro Wh + ) self.eauto = eauto self.haushaltsgeraet = haushaltsgeraet self.wechselrichter = wechselrichter - - - + def set_akku_discharge_hours(self, ds): self.akku.set_discharge_per_hour(ds) - + def set_eauto_charge_hours(self, ds): self.eauto.set_charge_per_hour(ds) def set_haushaltsgeraet_start(self, ds, global_start_hour=0): - self.haushaltsgeraet.set_startzeitpunkt(ds,global_start_hour=global_start_hour) - + self.haushaltsgeraet.set_startzeitpunkt(ds, global_start_hour=global_start_hour) + def reset(self): self.eauto.reset() self.akku.reset() @@ -58,19 +67,22 @@ def simuliere_ab_jetzt(self): # Beginne die Simulation ab der aktuellen Stunde und führe sie für die berechnete Dauer aus return self.simuliere(start_stunde) - - def simuliere(self, start_stunde): - - lastkurve_wh = self.gesamtlast # Anzahl der Stunden berechnen - assert len(lastkurve_wh) == len(self.pv_prognose_wh) == len(self.strompreis_euro_pro_wh), f"Arraygrößen stimmen nicht überein: Lastkurve = {len(lastkurve_wh)}, PV-Prognose = {len(self.pv_prognose_wh)}, Strompreis = {len(self.strompreis_euro_pro_wh)}" + assert ( + len(lastkurve_wh) + == len(self.pv_prognose_wh) + == len(self.strompreis_euro_pro_wh) + ), f"Arraygrößen stimmen nicht überein: Lastkurve = {len(lastkurve_wh)}, PV-Prognose = {len(self.pv_prognose_wh)}, Strompreis = {len(self.strompreis_euro_pro_wh)}" + + ende = min( + len(lastkurve_wh), + len(self.pv_prognose_wh), + len(self.strompreis_euro_pro_wh), + ) - ende = min( len(lastkurve_wh),len(self.pv_prognose_wh), len(self.strompreis_euro_pro_wh)) - - - total_hours = ende-start_stunde + total_hours = ende - start_stunde # Initialisierung der Arrays mit NaN-Werten last_wh_pro_stunde = np.full(total_hours, np.nan) @@ -88,13 +100,14 @@ def simuliere(self, start_stunde): if self.eauto: eauto_soc_pro_stunde[start_stunde] = self.eauto.ladezustand_in_prozent() - for stunde in range(start_stunde + 1, ende): - stunde_since_now = stunde-start_stunde - #print(stunde_since_now) + stunde_since_now = stunde - start_stunde + # print(stunde_since_now) # Anfangszustände akku_soc_start = self.akku.ladezustand_in_prozent() - eauto_soc_start = self.eauto.ladezustand_in_prozent() if self.eauto else None + eauto_soc_start = ( + self.eauto.ladezustand_in_prozent() if self.eauto else None + ) # Verbrauch und zusätzliche Lasten bestimmen verbrauch = self.gesamtlast[stunde] @@ -115,13 +128,19 @@ def simuliere(self, start_stunde): # E-Auto-Verbrauch bestimmen if self.eauto: - geladene_menge_eauto, verluste_eauto = self.eauto.energie_laden(None, stunde) + geladene_menge_eauto, verluste_eauto = self.eauto.energie_laden( + None, stunde + ) verbrauch += geladene_menge_eauto verluste_wh_pro_stunde[stunde_since_now] += verluste_eauto - eauto_soc_pro_stunde[stunde_since_now] = self.eauto.ladezustand_in_prozent() + eauto_soc_pro_stunde[stunde_since_now] = ( + self.eauto.ladezustand_in_prozent() + ) # Wechselrichter-Logik - netzeinspeisung, netzbezug, verluste, eigenverbrauch = self.wechselrichter.energie_verarbeiten(erzeugung, verbrauch, stunde) + netzeinspeisung, netzbezug, verluste, eigenverbrauch = ( + self.wechselrichter.energie_verarbeiten(erzeugung, verbrauch, stunde) + ) # Ergebnisse speichern netzeinspeisung_wh_pro_stunde[stunde_since_now] = netzeinspeisung @@ -130,30 +149,33 @@ def simuliere(self, start_stunde): last_wh_pro_stunde[stunde_since_now] = verbrauch # Finanzen berechnen kosten_euro_pro_stunde[stunde_since_now] = netzbezug * strompreis - einnahmen_euro_pro_stunde[stunde_since_now] = netzeinspeisung * self.einspeiseverguetung_euro_pro_wh[stunde] + einnahmen_euro_pro_stunde[stunde_since_now] = ( + netzeinspeisung * self.einspeiseverguetung_euro_pro_wh[stunde] + ) # Letzter Akkuzustand speichern akku_soc_pro_stunde[stunde_since_now] = self.akku.ladezustand_in_prozent() # Gesamtkosten berechnen - gesamtkosten_euro = np.nansum(kosten_euro_pro_stunde) - np.nansum(einnahmen_euro_pro_stunde) + gesamtkosten_euro = np.nansum(kosten_euro_pro_stunde) - np.nansum( + einnahmen_euro_pro_stunde + ) out = { - 'Last_Wh_pro_Stunde': last_wh_pro_stunde, - 'Netzeinspeisung_Wh_pro_Stunde': netzeinspeisung_wh_pro_stunde, - 'Netzbezug_Wh_pro_Stunde': netzbezug_wh_pro_stunde, - 'Kosten_Euro_pro_Stunde': kosten_euro_pro_stunde, - 'akku_soc_pro_stunde': akku_soc_pro_stunde, - 'Einnahmen_Euro_pro_Stunde': einnahmen_euro_pro_stunde, - 'Gesamtbilanz_Euro': gesamtkosten_euro, - 'E-Auto_SoC_pro_Stunde': eauto_soc_pro_stunde, - 'Gesamteinnahmen_Euro': np.nansum(einnahmen_euro_pro_stunde), - 'Gesamtkosten_Euro': np.nansum(kosten_euro_pro_stunde), + "Last_Wh_pro_Stunde": last_wh_pro_stunde, + "Netzeinspeisung_Wh_pro_Stunde": netzeinspeisung_wh_pro_stunde, + "Netzbezug_Wh_pro_Stunde": netzbezug_wh_pro_stunde, + "Kosten_Euro_pro_Stunde": kosten_euro_pro_stunde, + "akku_soc_pro_stunde": akku_soc_pro_stunde, + "Einnahmen_Euro_pro_Stunde": einnahmen_euro_pro_stunde, + "Gesamtbilanz_Euro": gesamtkosten_euro, + "E-Auto_SoC_pro_Stunde": eauto_soc_pro_stunde, + "Gesamteinnahmen_Euro": np.nansum(einnahmen_euro_pro_stunde), + "Gesamtkosten_Euro": np.nansum(kosten_euro_pro_stunde), "Verluste_Pro_Stunde": verluste_wh_pro_stunde, "Gesamt_Verluste": np.nansum(verluste_wh_pro_stunde), - "Haushaltsgeraet_wh_pro_stunde": haushaltsgeraet_wh_pro_stunde + "Haushaltsgeraet_wh_pro_stunde": haushaltsgeraet_wh_pro_stunde, } out = replace_nan_with_none(out) return out - diff --git a/modules/class_haushaltsgeraet.py b/modules/class_haushaltsgeraet.py index e1d5b0f..e1a37da 100644 --- a/modules/class_haushaltsgeraet.py +++ b/modules/class_haushaltsgeraet.py @@ -1,9 +1,12 @@ import numpy as np + class Haushaltsgeraet: def __init__(self, hours=None, verbrauch_kwh=None, dauer_h=None): self.hours = hours # Total duration for which the planning is done - self.verbrauch_kwh = verbrauch_kwh # Total energy consumption of the device in kWh + self.verbrauch_kwh = ( + verbrauch_kwh # Total energy consumption of the device in kWh + ) self.dauer_h = dauer_h # Duration of use in hours self.lastkurve = np.zeros(self.hours) # Initialize the load curve with zeros @@ -13,18 +16,18 @@ def set_startzeitpunkt(self, start_hour, global_start_hour=0): :param start_hour: The hour at which the device should start. """ self.reset() - + # Check if the duration of use is within the available time frame if start_hour + self.dauer_h > self.hours: raise ValueError("The duration of use exceeds the available time frame.") if start_hour < global_start_hour: raise ValueError("The start time is earlier than the available time frame.") - + # Calculate power per hour based on total consumption and duration - leistung_pro_stunde = (self.verbrauch_kwh / self.dauer_h) # Convert to watt-hours - + leistung_pro_stunde = self.verbrauch_kwh / self.dauer_h # Convert to watt-hours + # Set the power for the duration of use in the load curve array - self.lastkurve[start_hour:start_hour + self.dauer_h] = leistung_pro_stunde + self.lastkurve[start_hour : start_hour + self.dauer_h] = leistung_pro_stunde def reset(self): """ @@ -46,7 +49,7 @@ def get_last_fuer_stunde(self, hour): """ if hour < 0 or hour >= self.hours: raise ValueError("The specified hour is outside the available time frame.") - + return self.lastkurve[hour] def spaetestmoeglicher_startzeitpunkt(self): diff --git a/modules/class_heatpump.py b/modules/class_heatpump.py index 60321b9..84d6ac2 100644 --- a/modules/class_heatpump.py +++ b/modules/class_heatpump.py @@ -1,4 +1,3 @@ - class Heatpump: MAX_HEIZLEISTUNG = 5000 # Maximum heating power in watts BASE_HEIZLEISTUNG = 235.0 # Base heating power value @@ -17,20 +16,27 @@ def cop_berechnen(self, aussentemperatur): def heizleistung_berechnen(self, aussentemperatur): """Calculate heating power based on outside temperature.""" - heizleistung = ((self.BASE_HEIZLEISTUNG + aussentemperatur * self.TEMPERATURE_COEFFICIENT) * 1000) / 24.0 + heizleistung = ( + (self.BASE_HEIZLEISTUNG + aussentemperatur * self.TEMPERATURE_COEFFICIENT) + * 1000 + ) / 24.0 return min(self.max_heizleistung, heizleistung) def elektrische_leistung_berechnen(self, aussentemperatur): """Calculate electrical power based on outside temperature.""" - return 1164 - 77.8 * aussentemperatur + 1.62 * aussentemperatur ** 2.0 + return 1164 - 77.8 * aussentemperatur + 1.62 * aussentemperatur**2.0 def simulate_24h(self, temperaturen): """Simulate power data for 24 hours based on provided temperatures.""" leistungsdaten = [] if len(temperaturen) != self.prediction_hours: - raise ValueError("The temperature array must contain exactly " + str(self.prediction_hours) + " entries, one for each hour of the day.") - + raise ValueError( + "The temperature array must contain exactly " + + str(self.prediction_hours) + + " entries, one for each hour of the day." + ) + for temp in temperaturen: elektrische_leistung = self.elektrische_leistung_berechnen(temp) leistungsdaten.append(elektrische_leistung) @@ -38,7 +44,7 @@ def simulate_24h(self, temperaturen): # Example usage of the class -if __name__ == '__main__': +if __name__ == "__main__": max_heizleistung = 5000 # 5 kW heating power start_innentemperatur = 15 # Initial indoor temperature isolationseffizienz = 0.8 # Insulation efficiency @@ -49,7 +55,32 @@ def simulate_24h(self, temperaturen): print(wp.cop_berechnen(-10), " ", wp.cop_berechnen(0), " ", wp.cop_berechnen(10)) # 24 hours of outside temperatures (example values) - temperaturen = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, -1, -2, -3, -4, -5, -6, -7, -8, -9, -10, -5, -2, 5] + temperaturen = [ + 10, + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1, + 0, + -1, + -2, + -3, + -4, + -5, + -6, + -7, + -8, + -9, + -10, + -5, + -2, + 5, + ] # Calculate the 24-hour power data leistungsdaten = wp.simulate_24h(temperaturen) diff --git a/modules/class_inverter.py b/modules/class_inverter.py index bfa2f06..c487b53 100644 --- a/modules/class_inverter.py +++ b/modules/class_inverter.py @@ -1,6 +1,8 @@ class Wechselrichter: def __init__(self, max_leistung_wh, akku): - self.max_leistung_wh = max_leistung_wh # Maximum power that the inverter can handle + self.max_leistung_wh = ( + max_leistung_wh # Maximum power that the inverter can handle + ) self.akku = akku # Connection to a battery object def energie_verarbeiten(self, erzeugung, verbrauch, hour): @@ -14,15 +16,21 @@ def energie_verarbeiten(self, erzeugung, verbrauch, hour): # If consumption exceeds maximum inverter power verluste += erzeugung - self.max_leistung_wh restleistung_nach_verbrauch = self.max_leistung_wh - verbrauch - netzbezug = -restleistung_nach_verbrauch # Negative indicates feeding into the grid + netzbezug = ( + -restleistung_nach_verbrauch + ) # Negative indicates feeding into the grid eigenverbrauch = self.max_leistung_wh else: # Remaining power after consumption restleistung_nach_verbrauch = erzeugung - verbrauch - + # Load battery with excess energy - geladene_energie, verluste_laden_akku = self.akku.energie_laden(restleistung_nach_verbrauch, hour) - rest_überschuss = restleistung_nach_verbrauch - (geladene_energie + verluste_laden_akku) + geladene_energie, verluste_laden_akku = self.akku.energie_laden( + restleistung_nach_verbrauch, hour + ) + rest_überschuss = restleistung_nach_verbrauch - ( + geladene_energie + verluste_laden_akku + ) # Feed-in to the grid based on remaining capacity if rest_überschuss > self.max_leistung_wh - verbrauch: @@ -30,22 +38,30 @@ def energie_verarbeiten(self, erzeugung, verbrauch, hour): verluste += rest_überschuss - netzeinspeisung else: netzeinspeisung = rest_überschuss - + verluste += verluste_laden_akku eigenverbrauch = verbrauch # Self-consumption is equal to the load else: - benötigte_energie = verbrauch - erzeugung # Energy needed from external sources - max_akku_leistung = self.akku.max_ladeleistung_w # Maximum battery discharge power - + benötigte_energie = ( + verbrauch - erzeugung + ) # Energy needed from external sources + max_akku_leistung = ( + self.akku.max_ladeleistung_w + ) # Maximum battery discharge power + # Calculate remaining AC power available rest_ac_leistung = max(self.max_leistung_wh - erzeugung, 0) - + # Discharge energy from the battery based on need if benötigte_energie < rest_ac_leistung: - aus_akku, akku_entladeverluste = self.akku.energie_abgeben(benötigte_energie, hour) + aus_akku, akku_entladeverluste = self.akku.energie_abgeben( + benötigte_energie, hour + ) else: - aus_akku, akku_entladeverluste = self.akku.energie_abgeben(rest_ac_leistung, hour) + aus_akku, akku_entladeverluste = self.akku.energie_abgeben( + rest_ac_leistung, hour + ) verluste += akku_entladeverluste # Include losses from battery discharge netzbezug = benötigte_energie - aus_akku # Energy drawn from the grid diff --git a/modules/class_load.py b/modules/class_load.py index ec45f1b..ac280dc 100644 --- a/modules/class_load.py +++ b/modules/class_load.py @@ -1,9 +1,10 @@ -import numpy as np from datetime import datetime -from pprint import pprint + +import numpy as np # Load the .npz file when the application starts + class LoadForecast: def __init__(self, filepath=None, year_energy=None): self.filepath = filepath @@ -15,37 +16,41 @@ def __init__(self, filepath=None, year_energy=None): def get_daily_stats(self, date_str): """ Returns the 24-hour profile with mean and standard deviation for a given date. - + :param date_str: Date as a string in the format "YYYY-MM-DD" :return: An array with shape (2, 24), contains means and standard deviations """ # Convert the date string into a datetime object date = self._convert_to_datetime(date_str) - + # Calculate the day of the year (1 to 365) day_of_year = date.timetuple().tm_yday - + # Extract the 24-hour profile for the given date - daily_stats = self.data_year_energy[day_of_year - 1] # -1 because indexing starts at 0 + daily_stats = self.data_year_energy[ + day_of_year - 1 + ] # -1 because indexing starts at 0 return daily_stats def get_hourly_stats(self, date_str, hour): """ Returns the mean and standard deviation for a specific hour of a given date. - + :param date_str: Date as a string in the format "YYYY-MM-DD" :param hour: Specific hour (0 to 23) :return: An array with shape (2,), contains mean and standard deviation for the specified hour """ # Convert the date string into a datetime object date = self._convert_to_datetime(date_str) - + # Calculate the day of the year (1 to 365) day_of_year = date.timetuple().tm_yday - + # Extract mean and standard deviation for the given hour - hourly_stats = self.data_year_energy[day_of_year - 1, :, hour] # Access the specific hour - + hourly_stats = self.data_year_energy[ + day_of_year - 1, :, hour + ] # Access the specific hour + return hourly_stats def get_stats_for_date_range(self, start_date_str, end_date_str): @@ -58,12 +63,14 @@ def get_stats_for_date_range(self, start_date_str, end_date_str): """ start_date = self._convert_to_datetime(start_date_str) end_date = self._convert_to_datetime(end_date_str) - + start_day_of_year = start_date.timetuple().tm_yday end_day_of_year = end_date.timetuple().tm_yday # Note that in leap years, the day of the year may need adjustment - stats_for_range = self.data_year_energy[start_day_of_year:end_day_of_year] # -1 because indexing starts at 0 + stats_for_range = self.data_year_energy[ + start_day_of_year:end_day_of_year + ] # -1 because indexing starts at 0 stats_for_range = stats_for_range.swapaxes(1, 0) stats_for_range = stats_for_range.reshape(stats_for_range.shape[0], -1) @@ -73,7 +80,9 @@ def load_data(self): """Loads data from the specified file.""" try: data = np.load(self.filepath) - self.data = np.array(list(zip(data["yearly_profiles"], data["yearly_profiles_std"]))) + self.data = np.array( + list(zip(data["yearly_profiles"], data["yearly_profiles_std"])) + ) self.data_year_energy = self.data * self.year_energy # pprint(self.data_year_energy) except FileNotFoundError: @@ -89,10 +98,13 @@ def _convert_to_datetime(self, date_str): """Converts a date string to a datetime object.""" return datetime.strptime(date_str, "%Y-%m-%d") + # Example usage of the class -if __name__ == '__main__': - filepath = r'..\load_profiles.npz' # Adjust the path to the .npz file +if __name__ == "__main__": + filepath = r"..\load_profiles.npz" # Adjust the path to the .npz file lf = LoadForecast(filepath=filepath, year_energy=2000) - specific_date_prices = lf.get_daily_stats('2024-02-16') # Adjust date as needed - specific_hour_stats = lf.get_hourly_stats('2024-02-16', 12) # Adjust date and hour as needed + specific_date_prices = lf.get_daily_stats("2024-02-16") # Adjust date as needed + specific_hour_stats = lf.get_hourly_stats( + "2024-02-16", 12 + ) # Adjust date and hour as needed print(specific_hour_stats) diff --git a/modules/class_load_container.py b/modules/class_load_container.py index 801330e..f3c095f 100644 --- a/modules/class_load_container.py +++ b/modules/class_load_container.py @@ -1,36 +1,41 @@ import numpy as np -from pprint import pprint + class Gesamtlast: def __init__(self, prediction_hours=24): self.lasten = {} # Contains names and load arrays for different sources self.prediction_hours = prediction_hours - + def hinzufuegen(self, name, last_array): """ Adds an array of loads for a specific source. - + :param name: Name of the load source (e.g., "Household", "Heat Pump") :param last_array: Array of loads, where each entry corresponds to an hour """ if len(last_array) != self.prediction_hours: - raise ValueError(f"Total load inconsistent lengths in arrays: {name} {len(last_array)}") + raise ValueError( + f"Total load inconsistent lengths in arrays: {name} {len(last_array)}" + ) self.lasten[name] = last_array - + def gesamtlast_berechnen(self): """ Calculates the total load for each hour and returns an array of total loads. - + :return: Array of total loads, where each entry corresponds to an hour """ if not self.lasten: return [] - + # Assumption: All load arrays have the same length stunden = len(next(iter(self.lasten.values()))) gesamtlast_array = [0] * stunden - + for last_array in self.lasten.values(): - gesamtlast_array = [gesamtlast + stundenlast for gesamtlast, stundenlast in zip(gesamtlast_array, last_array)] - + gesamtlast_array = [ + gesamtlast + stundenlast + for gesamtlast, stundenlast in zip(gesamtlast_array, last_array) + ] + return np.array(gesamtlast_array) diff --git a/modules/class_load_corrector.py b/modules/class_load_corrector.py index e4bc27d..b599140 100644 --- a/modules/class_load_corrector.py +++ b/modules/class_load_corrector.py @@ -1,14 +1,11 @@ -import json import os import sys -from datetime import datetime, timedelta, timezone +import matplotlib.pyplot as plt import numpy as np import pandas as pd -import matplotlib.pyplot as plt from sklearn.metrics import mean_squared_error, r2_score -import mariadb # from sklearn.model_selection import train_test_split, GridSearchCV # from sklearn.ensemble import GradientBoostingRegressor # from xgboost import XGBRegressor @@ -22,6 +19,7 @@ # Add the parent directory to sys.path sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from config import * + from modules.class_load import * @@ -38,117 +36,159 @@ def __init__(self, measured_data, predicted_data, load_forecast): def _remove_outliers(self, data, threshold=2): # Calculate the Z-Score of the 'Last' data - data['Z-Score'] = np.abs((data['Last'] - data['Last'].mean()) / data['Last'].std()) + data["Z-Score"] = np.abs( + (data["Last"] - data["Last"].mean()) / data["Last"].std() + ) # Filter the data based on the threshold - filtered_data = data[data['Z-Score'] < threshold] - return filtered_data.drop(columns=['Z-Score']) + filtered_data = data[data["Z-Score"] < threshold] + return filtered_data.drop(columns=["Z-Score"]) def _merge_data(self): # Convert the time column in both DataFrames to datetime - self.predicted_data['time'] = pd.to_datetime(self.predicted_data['time']) - self.measured_data['time'] = pd.to_datetime(self.measured_data['time']) + self.predicted_data["time"] = pd.to_datetime(self.predicted_data["time"]) + self.measured_data["time"] = pd.to_datetime(self.measured_data["time"]) # Ensure both time columns have the same timezone - if self.measured_data['time'].dt.tz is None: - self.measured_data['time'] = self.measured_data['time'].dt.tz_localize('UTC') - - self.predicted_data['time'] = ( - self.predicted_data['time'].dt.tz_localize('UTC') - .dt.tz_convert('Europe/Berlin') + if self.measured_data["time"].dt.tz is None: + self.measured_data["time"] = self.measured_data["time"].dt.tz_localize( + "UTC" + ) + + self.predicted_data["time"] = ( + self.predicted_data["time"] + .dt.tz_localize("UTC") + .dt.tz_convert("Europe/Berlin") + ) + self.measured_data["time"] = self.measured_data["time"].dt.tz_convert( + "Europe/Berlin" ) - self.measured_data['time'] = self.measured_data['time'].dt.tz_convert('Europe/Berlin') # Optionally: Remove timezone information if only working locally - self.predicted_data['time'] = self.predicted_data['time'].dt.tz_localize(None) - self.measured_data['time'] = self.measured_data['time'].dt.tz_localize(None) + self.predicted_data["time"] = self.predicted_data["time"].dt.tz_localize(None) + self.measured_data["time"] = self.measured_data["time"].dt.tz_localize(None) # Now you can perform the merge - merged_data = pd.merge(self.measured_data, self.predicted_data, on='time', how='inner') + merged_data = pd.merge( + self.measured_data, self.predicted_data, on="time", how="inner" + ) print(merged_data) - merged_data['Hour'] = merged_data['time'].dt.hour - merged_data['DayOfWeek'] = merged_data['time'].dt.dayofweek + merged_data["Hour"] = merged_data["time"].dt.hour + merged_data["DayOfWeek"] = merged_data["time"].dt.dayofweek return merged_data def calculate_weighted_mean(self, train_period_weeks=9, test_period_weeks=1): self.merged_data = self._remove_outliers(self.merged_data) - train_end_date = self.merged_data['time'].max() - pd.Timedelta(weeks=test_period_weeks) + train_end_date = self.merged_data["time"].max() - pd.Timedelta( + weeks=test_period_weeks + ) train_start_date = train_end_date - pd.Timedelta(weeks=train_period_weeks) test_start_date = train_end_date + pd.Timedelta(hours=1) - test_end_date = test_start_date + pd.Timedelta(weeks=test_period_weeks) - pd.Timedelta(hours=1) + test_end_date = ( + test_start_date + + pd.Timedelta(weeks=test_period_weeks) + - pd.Timedelta(hours=1) + ) self.train_data = self.merged_data[ - (self.merged_data['time'] >= train_start_date) & - (self.merged_data['time'] <= train_end_date) + (self.merged_data["time"] >= train_start_date) + & (self.merged_data["time"] <= train_end_date) ] self.test_data = self.merged_data[ - (self.merged_data['time'] >= test_start_date) & - (self.merged_data['time'] <= test_end_date) + (self.merged_data["time"] >= test_start_date) + & (self.merged_data["time"] <= test_end_date) ] - self.train_data['Difference'] = self.train_data['Last'] - self.train_data['Last Pred'] + self.train_data["Difference"] = ( + self.train_data["Last"] - self.train_data["Last Pred"] + ) - weekdays_train_data = self.train_data[self.train_data['DayOfWeek'] < 5] - weekends_train_data = self.train_data[self.train_data['DayOfWeek'] >= 5] + weekdays_train_data = self.train_data[self.train_data["DayOfWeek"] < 5] + weekends_train_data = self.train_data[self.train_data["DayOfWeek"] >= 5] - self.weekday_diff = weekdays_train_data.groupby('Hour').apply(self._weighted_mean_diff).dropna() - self.weekend_diff = weekends_train_data.groupby('Hour').apply(self._weighted_mean_diff).dropna() + self.weekday_diff = ( + weekdays_train_data.groupby("Hour").apply(self._weighted_mean_diff).dropna() + ) + self.weekend_diff = ( + weekends_train_data.groupby("Hour").apply(self._weighted_mean_diff).dropna() + ) def _weighted_mean_diff(self, data): - train_end_date = self.train_data['time'].max() - weights = 1 / (train_end_date - data['time']).dt.days.replace(0, np.nan) - weighted_mean = (data['Difference'] * weights).sum() / weights.sum() + train_end_date = self.train_data["time"].max() + weights = 1 / (train_end_date - data["time"]).dt.days.replace(0, np.nan) + weighted_mean = (data["Difference"] * weights).sum() / weights.sum() return weighted_mean def adjust_predictions(self): - self.train_data['Adjusted Pred'] = self.train_data.apply(self._adjust_row, axis=1) - self.test_data['Adjusted Pred'] = self.test_data.apply(self._adjust_row, axis=1) + self.train_data["Adjusted Pred"] = self.train_data.apply( + self._adjust_row, axis=1 + ) + self.test_data["Adjusted Pred"] = self.test_data.apply(self._adjust_row, axis=1) def _adjust_row(self, row): - if row['DayOfWeek'] < 5: - return row['Last Pred'] + self.weekday_diff.get(row['Hour'], 0) + if row["DayOfWeek"] < 5: + return row["Last Pred"] + self.weekday_diff.get(row["Hour"], 0) else: - return row['Last Pred'] + self.weekend_diff.get(row['Hour'], 0) + return row["Last Pred"] + self.weekend_diff.get(row["Hour"], 0) def plot_results(self): - self._plot_data(self.train_data, 'Training') - self._plot_data(self.test_data, 'Testing') + self._plot_data(self.train_data, "Training") + self._plot_data(self.test_data, "Testing") def _plot_data(self, data, data_type): plt.figure(figsize=(14, 7)) - plt.plot(data['time'], data['Last'], label=f'Actual Last - {data_type}', color='blue') - plt.plot(data['time'], data['Last Pred'], label=f'Predicted Last - {data_type}', color='red', linestyle='--') - plt.plot(data['time'], data['Adjusted Pred'], label=f'Adjusted Predicted Last - {data_type}', color='green', linestyle=':') - plt.xlabel('Time') - plt.ylabel('Load') - plt.title(f'Actual vs Predicted vs Adjusted Predicted Load ({data_type} Data)') + plt.plot( + data["time"], data["Last"], label=f"Actual Last - {data_type}", color="blue" + ) + plt.plot( + data["time"], + data["Last Pred"], + label=f"Predicted Last - {data_type}", + color="red", + linestyle="--", + ) + plt.plot( + data["time"], + data["Adjusted Pred"], + label=f"Adjusted Predicted Last - {data_type}", + color="green", + linestyle=":", + ) + plt.xlabel("Time") + plt.ylabel("Load") + plt.title(f"Actual vs Predicted vs Adjusted Predicted Load ({data_type} Data)") plt.legend() plt.grid(True) plt.show() def evaluate_model(self): - mse = mean_squared_error(self.test_data['Last'], self.test_data['Adjusted Pred']) - r2 = r2_score(self.test_data['Last'], self.test_data['Adjusted Pred']) - print(f'Mean Squared Error: {mse}') - print(f'R-squared: {r2}') + mse = mean_squared_error( + self.test_data["Last"], self.test_data["Adjusted Pred"] + ) + r2 = r2_score(self.test_data["Last"], self.test_data["Adjusted Pred"]) + print(f"Mean Squared Error: {mse}") + print(f"R-squared: {r2}") def predict_next_hours(self, hours_ahead): - last_date = self.merged_data['time'].max() - future_dates = [last_date + pd.Timedelta(hours=i) for i in range(1, hours_ahead + 1)] - future_df = pd.DataFrame({'time': future_dates}) - future_df['Hour'] = future_df['time'].dt.hour - future_df['DayOfWeek'] = future_df['time'].dt.dayofweek - future_df['Last Pred'] = future_df['time'].apply(self._forecast_next_hours) - future_df['Adjusted Pred'] = future_df.apply(self._adjust_row, axis=1) + last_date = self.merged_data["time"].max() + future_dates = [ + last_date + pd.Timedelta(hours=i) for i in range(1, hours_ahead + 1) + ] + future_df = pd.DataFrame({"time": future_dates}) + future_df["Hour"] = future_df["time"].dt.hour + future_df["DayOfWeek"] = future_df["time"].dt.dayofweek + future_df["Last Pred"] = future_df["time"].apply(self._forecast_next_hours) + future_df["Adjusted Pred"] = future_df.apply(self._adjust_row, axis=1) return future_df def _forecast_next_hours(self, timestamp): - date_str = timestamp.strftime('%Y-%m-%d') + date_str = timestamp.strftime("%Y-%m-%d") hour = timestamp.hour daily_forecast = self.load_forecast.get_daily_stats(date_str) return daily_forecast[0][hour] if hour < len(daily_forecast[0]) else np.nan + # if __name__ == '__main__': # estimator = LastEstimator() # start_date = "2024-06-01" diff --git a/modules/class_optimize.py b/modules/class_optimize.py index 63ef5f6..9e433fa 100644 --- a/modules/class_optimize.py +++ b/modules/class_optimize.py @@ -1,31 +1,29 @@ -from flask import Flask, jsonify, request +import os + +import matplotlib import numpy as np -from modules.class_load import * -from modules.class_ems import * -from modules.class_pv_forecast import * -from modules.class_akku import * -from modules.class_heatpump import * -from modules.class_load_container import * -from modules.class_inverter import * +from modules.class_akku import * +from modules.class_ems import * +from modules.class_haushaltsgeraet import * +from modules.class_heatpump import * +from modules.class_inverter import * +from modules.class_load import * +from modules.class_load_container import * +from modules.class_pv_forecast import * from modules.class_sommerzeit import * from modules.visualize import * -from modules.class_haushaltsgeraet import * -import os -from flask import Flask, send_from_directory -from pprint import pprint -import matplotlib -matplotlib.use('Agg') # Setzt das Backend auf Agg -import matplotlib.pyplot as plt -import string -from datetime import datetime -from deap import base, creator, tools, algorithms -import numpy as np + +matplotlib.use("Agg") # Setzt das Backend auf Agg import random -import os +from datetime import datetime + +from deap import algorithms, base, creator, tools + sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from config import * + def isfloat(num): try: float(num) @@ -33,19 +31,29 @@ def isfloat(num): except: return False -def differential_evolution(population, toolbox, cxpb, mutpb, ngen, stats=None, halloffame=None, verbose=__debug__): + +def differential_evolution( + population, + toolbox, + cxpb, + mutpb, + ngen, + stats=None, + halloffame=None, + verbose=__debug__, +): """Differential Evolution Algorithm""" - + # Evaluate the entire population fitnesses = list(map(toolbox.evaluate, population)) for ind, fit in zip(population, fitnesses): ind.fitness.values = fit - + if halloffame is not None: halloffame.update(population) - + logbook = tools.Logbook() - logbook.header = ['gen', 'nevals'] + (stats.fields if stats else []) + logbook.header = ["gen", "nevals"] + (stats.fields if stats else []) for gen in range(ngen): # Generate the next generation by mutation and recombination @@ -56,10 +64,10 @@ def differential_evolution(population, toolbox, cxpb, mutpb, ngen, stats=None, h mutant[k] = c[k] + mutpb * (a[k] - b[k]) # Mutation step if random.random() < cxpb: # Recombination step mutant[k] = target[k] - + # Evaluate the mutant mutant.fitness.values = toolbox.evaluate(mutant) - + # Replace if mutant is better if mutant.fitness > target.fitness: population[i] = mutant @@ -73,345 +81,418 @@ def differential_evolution(population, toolbox, cxpb, mutpb, ngen, stats=None, h logbook.record(gen=gen, nevals=len(population), **record) if verbose: print(logbook.stream) - - return population, logbook + return population, logbook class optimization_problem: - def __init__(self, prediction_hours=24, strafe = 10, optimization_hours= 24): - self.prediction_hours = prediction_hours# + def __init__(self, prediction_hours=24, strafe=10, optimization_hours=24): + self.prediction_hours = prediction_hours # self.strafe = strafe self.opti_param = None - self.fixed_eauto_hours = prediction_hours-optimization_hours + self.fixed_eauto_hours = prediction_hours - optimization_hours self.possible_charge_values = moegliche_ladestroeme_in_prozent - def split_individual(self, individual): """ - Teilt das gegebene Individuum in die verschiedenen Parameter auf: + Teilt das gegebene Individuum in die verschiedenen Parameter auf: - Entladeparameter (discharge_hours_bin) - Ladeparameter (eautocharge_hours_float) - Haushaltsgeräte (spuelstart_int, falls vorhanden) """ # Extrahiere die Entlade- und Ladeparameter direkt aus dem Individuum - discharge_hours_bin = individual[:self.prediction_hours] # Erste 24 Werte sind Bool (Entladen) - eautocharge_hours_float = individual[self.prediction_hours:self.prediction_hours * 2] # Nächste 24 Werte sind Float (Laden) + discharge_hours_bin = individual[ + : self.prediction_hours + ] # Erste 24 Werte sind Bool (Entladen) + eautocharge_hours_float = individual[ + self.prediction_hours : self.prediction_hours * 2 + ] # Nächste 24 Werte sind Float (Laden) spuelstart_int = None if self.opti_param and self.opti_param.get("haushaltsgeraete", 0) > 0: - spuelstart_int = individual[-1] # Letzter Wert ist Startzeit für Haushaltsgerät + spuelstart_int = individual[ + -1 + ] # Letzter Wert ist Startzeit für Haushaltsgerät return discharge_hours_bin, eautocharge_hours_float, spuelstart_int - - def setup_deap_environment(self,opti_param, start_hour): + def setup_deap_environment(self, opti_param, start_hour): self.opti_param = opti_param - - + if "FitnessMin" in creator.__dict__: - del creator.FitnessMin + del creator.FitnessMin if "Individual" in creator.__dict__: - del creator.Individual + del creator.Individual creator.create("FitnessMin", base.Fitness, weights=(-1.0,)) creator.create("Individual", list, fitness=creator.FitnessMin) - + # PARAMETER self.toolbox = base.Toolbox() self.toolbox.register("attr_bool", random.randint, 0, 1) - self.toolbox.register("attr_float", random.uniform, 0, 1) # Für kontinuierliche Werte zwischen 0 und 1 (z.B. für E-Auto-Ladeleistung) - #self.toolbox.register("attr_choice", random.choice, self.possible_charge_values) # Für diskrete Ladeströme + self.toolbox.register( + "attr_float", random.uniform, 0, 1 + ) # Für kontinuierliche Werte zwischen 0 und 1 (z.B. für E-Auto-Ladeleistung) + # self.toolbox.register("attr_choice", random.choice, self.possible_charge_values) # Für diskrete Ladeströme self.toolbox.register("attr_int", random.randint, start_hour, 23) - - - + ################### # Haushaltsgeraete - #print("Haushalt:",opti_param["haushaltsgeraete"]) - if opti_param["haushaltsgeraete"]>0: - def create_individual(): - attrs = [self.toolbox.attr_bool() for _ in range(self.prediction_hours)] # 24 Bool-Werte für Entladen - attrs += [self.toolbox.attr_float() for _ in range(self.prediction_hours)] # 24 Float-Werte für Laden - attrs.append(self.toolbox.attr_int()) # Haushaltsgerät-Startzeit - return creator.Individual(attrs) + # print("Haushalt:",opti_param["haushaltsgeraete"]) + if opti_param["haushaltsgeraete"] > 0: + + def create_individual(): + attrs = [ + self.toolbox.attr_bool() for _ in range(self.prediction_hours) + ] # 24 Bool-Werte für Entladen + attrs += [ + self.toolbox.attr_float() for _ in range(self.prediction_hours) + ] # 24 Float-Werte für Laden + attrs.append(self.toolbox.attr_int()) # Haushaltsgerät-Startzeit + return creator.Individual(attrs) else: - def create_individual(): - attrs = [self.toolbox.attr_bool() for _ in range(self.prediction_hours)] # 24 Bool-Werte für Entladen - attrs += [self.toolbox.attr_float() for _ in range(self.prediction_hours)] # 24 Float-Werte für Laden - return creator.Individual(attrs) - - - self.toolbox.register("individual", create_individual)#tools.initCycle, creator.Individual, (self.toolbox.attr_bool,self.toolbox.attr_bool), n=self.prediction_hours+1) - self.toolbox.register("population", tools.initRepeat, list, self.toolbox.individual) + def create_individual(): + attrs = [ + self.toolbox.attr_bool() for _ in range(self.prediction_hours) + ] # 24 Bool-Werte für Entladen + attrs += [ + self.toolbox.attr_float() for _ in range(self.prediction_hours) + ] # 24 Float-Werte für Laden + return creator.Individual(attrs) + + self.toolbox.register( + "individual", create_individual + ) # tools.initCycle, creator.Individual, (self.toolbox.attr_bool,self.toolbox.attr_bool), n=self.prediction_hours+1) + self.toolbox.register( + "population", tools.initRepeat, list, self.toolbox.individual + ) self.toolbox.register("mate", tools.cxTwoPoint) self.toolbox.register("mutate", tools.mutFlipBit, indpb=0.1) - - - - #self.toolbox.register("mutate", mutate_choice, self.possible_charge_values, indpb=0.1) - #self.toolbox.register("mutate", tools.mutUniformInt, low=0, up=len(self.possible_charge_values)-1, indpb=0.1) - - self.toolbox.register("select", tools.selTournament, tournsize=3) - - def evaluate_inner(self,individual, ems,start_hour): - ems.reset() - - #print("Spuel:",self.opti_param) - discharge_hours_bin, eautocharge_hours_float, spuelstart_int = self.split_individual(individual) + # self.toolbox.register("mutate", mutate_choice, self.possible_charge_values, indpb=0.1) + # self.toolbox.register("mutate", tools.mutUniformInt, low=0, up=len(self.possible_charge_values)-1, indpb=0.1) - # Haushaltsgeraete - if self.opti_param["haushaltsgeraete"]>0: - ems.set_haushaltsgeraet_start(spuelstart_int,global_start_hour=start_hour) + self.toolbox.register("select", tools.selTournament, tournsize=3) + + def evaluate_inner(self, individual, ems, start_hour): + ems.reset() + # print("Spuel:",self.opti_param) + discharge_hours_bin, eautocharge_hours_float, spuelstart_int = ( + self.split_individual(individual) + ) - #discharge_hours_bin = np.full(self.prediction_hours,0) + # Haushaltsgeraete + if self.opti_param["haushaltsgeraete"] > 0: + ems.set_haushaltsgeraet_start(spuelstart_int, global_start_hour=start_hour) + + # discharge_hours_bin = np.full(self.prediction_hours,0) ems.set_akku_discharge_hours(discharge_hours_bin) - + # Setze die festen Werte für die letzten x Stunden - for i in range(self.prediction_hours - self.fixed_eauto_hours, self.prediction_hours): - eautocharge_hours_float[i] = 0.0 # Setze die letzten x Stunden auf einen festen Wert (oder vorgegebenen Wert) + for i in range( + self.prediction_hours - self.fixed_eauto_hours, self.prediction_hours + ): + eautocharge_hours_float[i] = ( + 0.0 # Setze die letzten x Stunden auf einen festen Wert (oder vorgegebenen Wert) + ) + + # print(eautocharge_hours_float) - #print(eautocharge_hours_float) - ems.set_eauto_charge_hours(eautocharge_hours_float) - - + o = ems.simuliere(start_hour) return o # Fitness-Funktion (muss Ihre EnergieManagementSystem-Logik integrieren) - def evaluate(self,individual,ems,parameter,start_hour,worst_case): - + def evaluate(self, individual, ems, parameter, start_hour, worst_case): try: - o = self.evaluate_inner(individual,ems,start_hour) - except: - return (100000.0,) - + o = self.evaluate_inner(individual, ems, start_hour) + except: + return (100000.0,) + gesamtbilanz = o["Gesamtbilanz_Euro"] if worst_case: - gesamtbilanz = gesamtbilanz * -1.0 - - discharge_hours_bin, eautocharge_hours_float, spuelstart_int = self.split_individual(individual) + gesamtbilanz = gesamtbilanz * -1.0 + + discharge_hours_bin, eautocharge_hours_float, spuelstart_int = ( + self.split_individual(individual) + ) max_ladeleistung = np.max(moegliche_ladestroeme_in_prozent) strafe_überschreitung = 0.0 # Ladeleistung überschritten? for ladeleistung in eautocharge_hours_float: - if ladeleistung > max_ladeleistung: - # Berechne die Überschreitung - überschreitung = ladeleistung - max_ladeleistung - # Füge eine Strafe hinzu (z.B. 10 Einheiten Strafe pro Prozentpunkt Überschreitung) - strafe_überschreitung += self.strafe * 10 # Hier ist die Strafe proportional zur Überschreitung + if ladeleistung > max_ladeleistung: + # Berechne die Überschreitung + überschreitung = ladeleistung - max_ladeleistung + # Füge eine Strafe hinzu (z.B. 10 Einheiten Strafe pro Prozentpunkt Überschreitung) + strafe_überschreitung += ( + self.strafe * 10 + ) # Hier ist die Strafe proportional zur Überschreitung - # Für jeden Discharge 0, eine kleine Strafe von 1 Cent, da die Lastvertelung noch fehlt. Also wenn es egal ist, soll er den Akku entladen lassen for i in range(0, self.prediction_hours): - if discharge_hours_bin[i] == 0.0: # Wenn die letzten x Stunden von einem festen Wert abweichen + if ( + discharge_hours_bin[i] == 0.0 + ): # Wenn die letzten x Stunden von einem festen Wert abweichen gesamtbilanz += 0.01 # Bestrafe den Optimierer - - - # E-Auto nur die ersten self.fixed_eauto_hours - for i in range(self.prediction_hours - self.fixed_eauto_hours, self.prediction_hours): - if eautocharge_hours_float[i] != 0.0: # Wenn die letzten x Stunden von einem festen Wert abweichen + + # E-Auto nur die ersten self.fixed_eauto_hours + for i in range( + self.prediction_hours - self.fixed_eauto_hours, self.prediction_hours + ): + if ( + eautocharge_hours_float[i] != 0.0 + ): # Wenn die letzten x Stunden von einem festen Wert abweichen gesamtbilanz += self.strafe # Bestrafe den Optimierer - - + # Überprüfung, ob der Mindest-SoC erreicht wird - final_soc = ems.eauto.ladezustand_in_prozent() # Nimmt den SoC am Ende des Optimierungszeitraums - - if (parameter['eauto_min_soc']-ems.eauto.ladezustand_in_prozent()) <= 0.0: - #print (parameter['eauto_min_soc']," " ,ems.eauto.ladezustand_in_prozent()," ",(parameter['eauto_min_soc']-ems.eauto.ladezustand_in_prozent())) - for i in range(0, self.prediction_hours): - if eautocharge_hours_float[i] != 0.0: # Wenn die letzten x Stunden von einem festen Wert abweichen - gesamtbilanz += self.strafe # Bestrafe den Optimierer - - - eauto_roi = (parameter['eauto_min_soc']-ems.eauto.ladezustand_in_prozent()) - individual.extra_data = (o["Gesamtbilanz_Euro"],o["Gesamt_Verluste"], eauto_roi ) - - + final_soc = ( + ems.eauto.ladezustand_in_prozent() + ) # Nimmt den SoC am Ende des Optimierungszeitraums + + if (parameter["eauto_min_soc"] - ems.eauto.ladezustand_in_prozent()) <= 0.0: + # print (parameter['eauto_min_soc']," " ,ems.eauto.ladezustand_in_prozent()," ",(parameter['eauto_min_soc']-ems.eauto.ladezustand_in_prozent())) + for i in range(0, self.prediction_hours): + if ( + eautocharge_hours_float[i] != 0.0 + ): # Wenn die letzten x Stunden von einem festen Wert abweichen + gesamtbilanz += self.strafe # Bestrafe den Optimierer + + eauto_roi = parameter["eauto_min_soc"] - ems.eauto.ladezustand_in_prozent() + individual.extra_data = ( + o["Gesamtbilanz_Euro"], + o["Gesamt_Verluste"], + eauto_roi, + ) + restenergie_akku = ems.akku.aktueller_energieinhalt() - restwert_akku = restenergie_akku*parameter["preis_euro_pro_wh_akku"] + restwert_akku = restenergie_akku * parameter["preis_euro_pro_wh_akku"] # print(restenergie_akku) # print(parameter["preis_euro_pro_wh_akku"]) # print(restwert_akku) # print() strafe = 0.0 - strafe = max(0,(parameter['eauto_min_soc']-ems.eauto.ladezustand_in_prozent()) * self.strafe ) - gesamtbilanz += strafe - restwert_akku + strafe_überschreitung - #gesamtbilanz += o["Gesamt_Verluste"]/10000.0 - - return (gesamtbilanz,) - - - + strafe = max( + 0, + (parameter["eauto_min_soc"] - ems.eauto.ladezustand_in_prozent()) + * self.strafe, + ) + gesamtbilanz += strafe - restwert_akku + strafe_überschreitung + # gesamtbilanz += o["Gesamt_Verluste"]/10000.0 + return (gesamtbilanz,) # Genetischer Algorithmus - def optimize(self,start_solution=None): - - + def optimize(self, start_solution=None): population = self.toolbox.population(n=300) hof = tools.HallOfFame(1) - + stats = tools.Statistics(lambda ind: ind.fitness.values) stats.register("avg", np.mean) stats.register("min", np.min) stats.register("max", np.max) - - print("Start:",start_solution) - + + print("Start:", start_solution) + if start_solution is not None and start_solution != -1: - population.insert(0, creator.Individual(start_solution)) - population.insert(1, creator.Individual(start_solution)) - population.insert(2, creator.Individual(start_solution)) - - algorithms.eaMuPlusLambda(population, self.toolbox, mu=100, lambda_=200, cxpb=0.5, mutpb=0.3, ngen=400, stats=stats, halloffame=hof, verbose=True) - #algorithms.eaSimple(population, self.toolbox, cxpb=0.3, mutpb=0.3, ngen=200, stats=stats, halloffame=hof, verbose=True) - #algorithms.eaMuCommaLambda(population, self.toolbox, mu=100, lambda_=200, cxpb=0.2, mutpb=0.4, ngen=300, stats=stats, halloffame=hof, verbose=True) - #population, log = differential_evolution(population, self.toolbox, cxpb=0.2, mutpb=0.5, ngen=200, stats=stats, halloffame=hof, verbose=True) - - - - - - member = {"bilanz":[],"verluste":[],"nebenbedingung":[]} + population.insert(0, creator.Individual(start_solution)) + population.insert(1, creator.Individual(start_solution)) + population.insert(2, creator.Individual(start_solution)) + + algorithms.eaMuPlusLambda( + population, + self.toolbox, + mu=100, + lambda_=200, + cxpb=0.5, + mutpb=0.3, + ngen=400, + stats=stats, + halloffame=hof, + verbose=True, + ) + # algorithms.eaSimple(population, self.toolbox, cxpb=0.3, mutpb=0.3, ngen=200, stats=stats, halloffame=hof, verbose=True) + # algorithms.eaMuCommaLambda(population, self.toolbox, mu=100, lambda_=200, cxpb=0.2, mutpb=0.4, ngen=300, stats=stats, halloffame=hof, verbose=True) + # population, log = differential_evolution(population, self.toolbox, cxpb=0.2, mutpb=0.5, ngen=200, stats=stats, halloffame=hof, verbose=True) + + member = {"bilanz": [], "verluste": [], "nebenbedingung": []} for ind in population: - if hasattr(ind, 'extra_data'): - extra_value1, extra_value2,extra_value3 = ind.extra_data - member["bilanz"].append(extra_value1) - member["verluste"].append(extra_value2) - member["nebenbedingung"].append(extra_value3) - - - return hof[0], member - + if hasattr(ind, "extra_data"): + extra_value1, extra_value2, extra_value3 = ind.extra_data + member["bilanz"].append(extra_value1) + member["verluste"].append(extra_value2) + member["nebenbedingung"].append(extra_value3) - def optimierung_ems(self,parameter=None, start_hour=None,worst_case=False, startdate=None): + return hof[0], member - + def optimierung_ems( + self, parameter=None, start_hour=None, worst_case=False, startdate=None + ): ############ - # Parameter + # Parameter ############ if startdate == None: - date = (datetime.now().date() + timedelta(hours = self.prediction_hours)).strftime("%Y-%m-%d") - date_now = datetime.now().strftime("%Y-%m-%d") + date = ( + datetime.now().date() + timedelta(hours=self.prediction_hours) + ).strftime("%Y-%m-%d") + date_now = datetime.now().strftime("%Y-%m-%d") else: - date = (startdate + timedelta(hours = self.prediction_hours)).strftime("%Y-%m-%d") - date_now = startdate.strftime("%Y-%m-%d") - #print("Start_date:",date_now) - - akku_size = parameter['pv_akku_cap'] # Wh - - einspeiseverguetung_euro_pro_wh = np.full(self.prediction_hours, parameter["einspeiseverguetung_euro_pro_wh"]) #= # € / Wh 7/(1000.0*100.0) - discharge_array = np.full(self.prediction_hours,1) #np.array([1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0]) # - akku = PVAkku(kapazitaet_wh=akku_size,hours=self.prediction_hours,start_soc_prozent=parameter["pv_soc"], max_ladeleistung_w=5000) + date = (startdate + timedelta(hours=self.prediction_hours)).strftime( + "%Y-%m-%d" + ) + date_now = startdate.strftime("%Y-%m-%d") + # print("Start_date:",date_now) + + akku_size = parameter["pv_akku_cap"] # Wh + + einspeiseverguetung_euro_pro_wh = np.full( + self.prediction_hours, parameter["einspeiseverguetung_euro_pro_wh"] + ) # = # € / Wh 7/(1000.0*100.0) + discharge_array = np.full( + self.prediction_hours, 1 + ) # np.array([1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0]) # + akku = PVAkku( + kapazitaet_wh=akku_size, + hours=self.prediction_hours, + start_soc_prozent=parameter["pv_soc"], + max_ladeleistung_w=5000, + ) akku.set_charge_per_hour(discharge_array) - - - laden_moeglich = np.full(self.prediction_hours,1) # np.array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0]) - eauto = PVAkku(kapazitaet_wh=parameter["eauto_cap"], hours=self.prediction_hours, lade_effizienz=parameter["eauto_charge_efficiency"], entlade_effizienz=1.0, max_ladeleistung_w=parameter["eauto_charge_power"] ,start_soc_prozent=parameter["eauto_soc"]) + + laden_moeglich = np.full( + self.prediction_hours, 1 + ) # np.array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0]) + eauto = PVAkku( + kapazitaet_wh=parameter["eauto_cap"], + hours=self.prediction_hours, + lade_effizienz=parameter["eauto_charge_efficiency"], + entlade_effizienz=1.0, + max_ladeleistung_w=parameter["eauto_charge_power"], + start_soc_prozent=parameter["eauto_soc"], + ) eauto.set_charge_per_hour(laden_moeglich) - min_soc_eauto = parameter['eauto_min_soc'] - start_params = parameter['start_solution'] - + min_soc_eauto = parameter["eauto_min_soc"] + start_params = parameter["start_solution"] + ############### # spuelmaschine ############## print(parameter) - if parameter["haushaltsgeraet_dauer"] >0: - spuelmaschine = Haushaltsgeraet(hours=self.prediction_hours, verbrauch_kwh=parameter["haushaltsgeraet_wh"], dauer_h=parameter["haushaltsgeraet_dauer"]) - spuelmaschine.set_startzeitpunkt(start_hour) # Startet jetzt - else: - spuelmaschine = None - - - - - - - - + if parameter["haushaltsgeraet_dauer"] > 0: + spuelmaschine = Haushaltsgeraet( + hours=self.prediction_hours, + verbrauch_kwh=parameter["haushaltsgeraet_wh"], + dauer_h=parameter["haushaltsgeraet_dauer"], + ) + spuelmaschine.set_startzeitpunkt(start_hour) # Startet jetzt + else: + spuelmaschine = None ############### # PV Forecast ############### - #PVforecast = PVForecast(filepath=os.path.join(r'test_data', r'pvprognose.json')) + # PVforecast = PVForecast(filepath=os.path.join(r'test_data', r'pvprognose.json')) # PVforecast = PVForecast(prediction_hours = self.prediction_hours, url=pv_forecast_url) # #print("PVPOWER",parameter['pvpowernow']) # if isfloat(parameter['pvpowernow']): - # PVforecast.update_ac_power_measurement(date_time=datetime.now(), ac_power_measurement=float(parameter['pvpowernow'])) - # #PVforecast.print_ac_power_and_measurement() - pv_forecast = parameter['pv_forecast'] #PVforecast.get_pv_forecast_for_date_range(date_now,date) #get_forecast_for_date(date) - temperature_forecast = parameter['temperature_forecast'] #PVforecast.get_temperature_for_date_range(date_now,date) - + # PVforecast.update_ac_power_measurement(date_time=datetime.now(), ac_power_measurement=float(parameter['pvpowernow'])) + # #PVforecast.print_ac_power_and_measurement() + pv_forecast = parameter[ + "pv_forecast" + ] # PVforecast.get_pv_forecast_for_date_range(date_now,date) #get_forecast_for_date(date) + temperature_forecast = parameter[ + "temperature_forecast" + ] # PVforecast.get_temperature_for_date_range(date_now,date) ############### - # Strompreise + # Strompreise ############### specific_date_prices = parameter["strompreis_euro_pro_wh"] print(specific_date_prices) - #print("https://api.akkudoktor.net/prices?start="+date_now+"&end="+date) - + # print("https://api.akkudoktor.net/prices?start="+date_now+"&end="+date) wr = Wechselrichter(10000, akku) - ems = EnergieManagementSystem(gesamtlast = parameter["gesamtlast"], pv_prognose_wh=pv_forecast, strompreis_euro_pro_wh=specific_date_prices, einspeiseverguetung_euro_pro_wh=einspeiseverguetung_euro_pro_wh, eauto=eauto, haushaltsgeraet=spuelmaschine,wechselrichter=wr) + ems = EnergieManagementSystem( + gesamtlast=parameter["gesamtlast"], + pv_prognose_wh=pv_forecast, + strompreis_euro_pro_wh=specific_date_prices, + einspeiseverguetung_euro_pro_wh=einspeiseverguetung_euro_pro_wh, + eauto=eauto, + haushaltsgeraet=spuelmaschine, + wechselrichter=wr, + ) o = ems.simuliere(start_hour) - + ############### # Optimizer Init ############## opti_param = {} opti_param["haushaltsgeraete"] = 0 if spuelmaschine != None: - opti_param["haushaltsgeraete"] = 1 - + opti_param["haushaltsgeraete"] = 1 + self.setup_deap_environment(opti_param, start_hour) def evaluate_wrapper(individual): - return self.evaluate(individual, ems, parameter,start_hour,worst_case) - + return self.evaluate(individual, ems, parameter, start_hour, worst_case) + self.toolbox.register("evaluate", evaluate_wrapper) start_solution, extra_data = self.optimize(start_params) best_solution = start_solution - o = self.evaluate_inner(best_solution, ems,start_hour) + o = self.evaluate_inner(best_solution, ems, start_hour) eauto = ems.eauto.to_dict() spuelstart_int = None - discharge_hours_bin, eautocharge_hours_float, spuelstart_int = self.split_individual(best_solution) - - + discharge_hours_bin, eautocharge_hours_float, spuelstart_int = ( + self.split_individual(best_solution) + ) + print(parameter) print(best_solution) - visualisiere_ergebnisse(parameter["gesamtlast"], pv_forecast, specific_date_prices, o,discharge_hours_bin,eautocharge_hours_float , temperature_forecast, start_hour, self.prediction_hours,einspeiseverguetung_euro_pro_wh,extra_data=extra_data) + visualisiere_ergebnisse( + parameter["gesamtlast"], + pv_forecast, + specific_date_prices, + o, + discharge_hours_bin, + eautocharge_hours_float, + temperature_forecast, + start_hour, + self.prediction_hours, + einspeiseverguetung_euro_pro_wh, + extra_data=extra_data, + ) os.system("cp visualisierungsergebnisse.pdf ~/") - - # 'Eigenverbrauch_Wh_pro_Stunde': eigenverbrauch_wh_pro_stunde, - # 'Netzeinspeisung_Wh_pro_Stunde': netzeinspeisung_wh_pro_stunde, - # 'Netzbezug_Wh_pro_Stunde': netzbezug_wh_pro_stunde, - # 'Kosten_Euro_pro_Stunde': kosten_euro_pro_stunde, - # 'akku_soc_pro_stunde': akku_soc_pro_stunde, - # 'Einnahmen_Euro_pro_Stunde': einnahmen_euro_pro_stunde, - # 'Gesamtbilanz_Euro': gesamtkosten_euro, - # 'E-Auto_SoC_pro_Stunde':eauto_soc_pro_stunde, - # 'Gesamteinnahmen_Euro': sum(einnahmen_euro_pro_stunde), - # 'Gesamtkosten_Euro': sum(kosten_euro_pro_stunde), - # "Verluste_Pro_Stunde":verluste_wh_pro_stunde, - # "Gesamt_Verluste":sum(verluste_wh_pro_stunde), - # "Haushaltsgeraet_wh_pro_stunde":haushaltsgeraet_wh_pro_stunde - - #print(eauto) - return {"discharge_hours_bin":discharge_hours_bin, "eautocharge_hours_float":eautocharge_hours_float ,"result":o ,"eauto_obj":eauto,"start_solution":best_solution,"spuelstart":spuelstart_int,"simulation_data":o} - - - - + # 'Eigenverbrauch_Wh_pro_Stunde': eigenverbrauch_wh_pro_stunde, + # 'Netzeinspeisung_Wh_pro_Stunde': netzeinspeisung_wh_pro_stunde, + # 'Netzbezug_Wh_pro_Stunde': netzbezug_wh_pro_stunde, + # 'Kosten_Euro_pro_Stunde': kosten_euro_pro_stunde, + # 'akku_soc_pro_stunde': akku_soc_pro_stunde, + # 'Einnahmen_Euro_pro_Stunde': einnahmen_euro_pro_stunde, + # 'Gesamtbilanz_Euro': gesamtkosten_euro, + # 'E-Auto_SoC_pro_Stunde':eauto_soc_pro_stunde, + # 'Gesamteinnahmen_Euro': sum(einnahmen_euro_pro_stunde), + # 'Gesamtkosten_Euro': sum(kosten_euro_pro_stunde), + # "Verluste_Pro_Stunde":verluste_wh_pro_stunde, + # "Gesamt_Verluste":sum(verluste_wh_pro_stunde), + # "Haushaltsgeraet_wh_pro_stunde":haushaltsgeraet_wh_pro_stunde + + # print(eauto) + return { + "discharge_hours_bin": discharge_hours_bin, + "eautocharge_hours_float": eautocharge_hours_float, + "result": o, + "eauto_obj": eauto, + "start_solution": best_solution, + "spuelstart": spuelstart_int, + "simulation_data": o, + } diff --git a/modules/class_pv_forecast.py b/modules/class_pv_forecast.py index 86fd30c..ca9b86e 100644 --- a/modules/class_pv_forecast.py +++ b/modules/class_pv_forecast.py @@ -1,15 +1,25 @@ -from flask import Flask, jsonify, request -import numpy as np -from datetime import datetime, timedelta +import hashlib +import json +import os +from datetime import datetime from pprint import pprint -import json, sys, os -import requests, hashlib -from dateutil import parser + +import numpy as np import pandas as pd +import requests +from dateutil import parser class ForecastData: - def __init__(self, date_time, dc_power, ac_power, windspeed_10m=None, temperature=None, ac_power_measurement=None): + def __init__( + self, + date_time, + dc_power, + ac_power, + windspeed_10m=None, + temperature=None, + ac_power_measurement=None, + ): self.date_time = date_time self.dc_power = dc_power self.ac_power = ac_power @@ -38,8 +48,9 @@ def get_windspeed_10m(self): def get_temperature(self): return self.temperature + class PVForecast: - def __init__(self, filepath=None, url=None, cache_dir='cache', prediction_hours=48): + def __init__(self, filepath=None, url=None, cache_dir="cache", prediction_hours=48): self.meta = {} self.forecast_data = [] self.cache_dir = cache_dir @@ -54,51 +65,56 @@ def __init__(self, filepath=None, url=None, cache_dir='cache', prediction_hours= self.load_data_with_caching(url) if len(self.forecast_data) < self.prediction_hours: - raise ValueError(f"Die Vorhersage muss mindestens {self.prediction_hours} Stunden umfassen, aber es wurden nur {len(self.forecast_data)} Stunden vorhergesagt.") + raise ValueError( + f"Die Vorhersage muss mindestens {self.prediction_hours} Stunden umfassen, aber es wurden nur {len(self.forecast_data)} Stunden vorhergesagt." + ) def update_ac_power_measurement(self, date_time=None, ac_power_measurement=None): found = False input_date_hour = date_time.replace(minute=0, second=0, microsecond=0) for forecast in self.forecast_data: - forecast_date_hour = parser.parse(forecast.date_time).replace(minute=0, second=0, microsecond=0) + forecast_date_hour = parser.parse(forecast.date_time).replace( + minute=0, second=0, microsecond=0 + ) if forecast_date_hour == input_date_hour: forecast.ac_power_measurement = ac_power_measurement found = True break def process_data(self, data): - self.meta = data.get('meta', {}) - all_values = data.get('values', []) - - for i in range(len(all_values[0])): # Annahme, dass alle Listen gleich lang sind - sum_dc_power = sum(values[i]['dcPower'] for values in all_values) - sum_ac_power = sum(values[i]['power'] for values in all_values) + self.meta = data.get("meta", {}) + all_values = data.get("values", []) + + for i in range( + len(all_values[0]) + ): # Annahme, dass alle Listen gleich lang sind + sum_dc_power = sum(values[i]["dcPower"] for values in all_values) + sum_ac_power = sum(values[i]["power"] for values in all_values) # Zeige die ursprünglichen und berechneten Zeitstempel an - original_datetime = all_values[0][i].get('datetime') - #print(original_datetime," ",sum_dc_power," ",all_values[0][i]['dcPower']) + original_datetime = all_values[0][i].get("datetime") + # print(original_datetime," ",sum_dc_power," ",all_values[0][i]['dcPower']) dt = datetime.strptime(original_datetime, "%Y-%m-%dT%H:%M:%S.%f%z") dt = dt.replace(tzinfo=None) - #iso_datetime = parser.parse(original_datetime).isoformat() # Konvertiere zu ISO-Format - #print() + # iso_datetime = parser.parse(original_datetime).isoformat() # Konvertiere zu ISO-Format + # print() # Optional: 2 Stunden abziehen, um die Zeitanpassung zu testen - #adjusted_datetime = parser.parse(original_datetime) - timedelta(hours=2) - #print(f"Angepasste Zeitstempel: {adjusted_datetime.isoformat()}") + # adjusted_datetime = parser.parse(original_datetime) - timedelta(hours=2) + # print(f"Angepasste Zeitstempel: {adjusted_datetime.isoformat()}") forecast = ForecastData( date_time=dt, # Verwende angepassten Zeitstempel dc_power=sum_dc_power, ac_power=sum_ac_power, - windspeed_10m=all_values[0][i].get('windspeed_10m'), - temperature=all_values[0][i].get('temperature') + windspeed_10m=all_values[0][i].get("windspeed_10m"), + temperature=all_values[0][i].get("temperature"), ) - self.forecast_data.append(forecast) def load_data_from_file(self, filepath): - with open(filepath, 'r') as file: + with open(filepath, "r") as file: data = json.load(file) self.process_data(data) @@ -109,31 +125,37 @@ def load_data_from_url(self, url): pprint(data) self.process_data(data) else: - print(f"Failed to load data from {url}. Status Code: {response.status_code}") + print( + f"Failed to load data from {url}. Status Code: {response.status_code}" + ) self.load_data_from_url(url) def load_data_with_caching(self, url): date = datetime.now().strftime("%Y-%m-%d") - cache_file = os.path.join(self.cache_dir, self.generate_cache_filename(url, date)) + cache_file = os.path.join( + self.cache_dir, self.generate_cache_filename(url, date) + ) if os.path.exists(cache_file): - with open(cache_file, 'r') as file: + with open(cache_file, "r") as file: data = json.load(file) print("Loading data from cache.") else: response = requests.get(url) if response.status_code == 200: data = response.json() - with open(cache_file, 'w') as file: + with open(cache_file, "w") as file: json.dump(data, file) print("Data fetched from URL and cached.") else: - print(f"Failed to load data from {url}. Status Code: {response.status_code}") + print( + f"Failed to load data from {url}. Status Code: {response.status_code}" + ) return self.process_data(data) def generate_cache_filename(self, url, date): - cache_key = hashlib.sha256(f"{url}{date}".encode('utf-8')).hexdigest() + cache_key = hashlib.sha256(f"{url}{date}".encode("utf-8")).hexdigest() return f"cache_{cache_key}.json" def get_forecast_data(self): @@ -141,11 +163,15 @@ def get_forecast_data(self): def get_temperature_forecast_for_date(self, input_date_str): input_date = datetime.strptime(input_date_str, "%Y-%m-%d") - daily_forecast_obj = [data for data in self.forecast_data if parser.parse(data.get_date_time()).date() == input_date.date()] + daily_forecast_obj = [ + data + for data in self.forecast_data + if parser.parse(data.get_date_time()).date() == input_date.date() + ] daily_forecast = [] for d in daily_forecast_obj: daily_forecast.append(d.get_temperature()) - + return np.array(daily_forecast) def get_pv_forecast_for_date_range(self, start_date_str, end_date_str): @@ -154,51 +180,65 @@ def get_pv_forecast_for_date_range(self, start_date_str, end_date_str): date_range_forecast = [] for data in self.forecast_data: - data_date = data.get_date_time().date()#parser.parse(data.get_date_time()).date() + data_date = ( + data.get_date_time().date() + ) # parser.parse(data.get_date_time()).date() if start_date <= data_date <= end_date: date_range_forecast.append(data) - print(data.get_date_time()," ",data.get_ac_power()) - - ac_power_forecast = np.array([data.get_ac_power() for data in date_range_forecast]) + print(data.get_date_time(), " ", data.get_ac_power()) + + ac_power_forecast = np.array( + [data.get_ac_power() for data in date_range_forecast] + ) + + return np.array(ac_power_forecast)[: self.prediction_hours] - return np.array(ac_power_forecast)[:self.prediction_hours] - def get_temperature_for_date_range(self, start_date_str, end_date_str): start_date = datetime.strptime(start_date_str, "%Y-%m-%d").date() end_date = datetime.strptime(end_date_str, "%Y-%m-%d").date() date_range_forecast = [] - + for data in self.forecast_data: data_date = data.get_date_time().date() if start_date <= data_date <= end_date: date_range_forecast.append(data) - + temperature_forecast = [data.get_temperature() for data in date_range_forecast] - return np.array(temperature_forecast)[:self.prediction_hours] + return np.array(temperature_forecast)[: self.prediction_hours] def get_forecast_dataframe(self): # Wandelt die Vorhersagedaten in ein Pandas DataFrame um - data = [{ - 'date_time': f.get_date_time(), - 'dc_power': f.get_dc_power(), - 'ac_power': f.get_ac_power(), - 'windspeed_10m': f.get_windspeed_10m(), - 'temperature': f.get_temperature() - } for f in self.forecast_data] + data = [ + { + "date_time": f.get_date_time(), + "dc_power": f.get_dc_power(), + "ac_power": f.get_ac_power(), + "windspeed_10m": f.get_windspeed_10m(), + "temperature": f.get_temperature(), + } + for f in self.forecast_data + ] # Erstelle ein DataFrame df = pd.DataFrame(data) return df - def print_ac_power_and_measurement(self): """Druckt die DC-Leistung und den Messwert für jede Stunde.""" for forecast in self.forecast_data: date_time = forecast.date_time - print(f"Zeit: {date_time}, DC: {forecast.dc_power}, AC: {forecast.ac_power}, Messwert: {forecast.ac_power_measurement}, AC GET: {forecast.get_ac_power()}") + print( + f"Zeit: {date_time}, DC: {forecast.dc_power}, AC: {forecast.ac_power}, Messwert: {forecast.ac_power_measurement}, AC GET: {forecast.get_ac_power()}" + ) + # Beispiel für die Verwendung der Klasse -if __name__ == '__main__': - forecast = PVForecast(prediction_hours=24, url="https://api.akkudoktor.net/forecast?lat=50.8588&lon=7.3747&power=5000&azimuth=-10&tilt=7&powerInvertor=10000&horizont=20,27,22,20&power=4800&azimuth=-90&tilt=7&powerInvertor=10000&horizont=30,30,30,50&power=1400&azimuth=-40&tilt=60&powerInvertor=2000&horizont=60,30,0,30&power=1600&azimuth=5&tilt=45&powerInvertor=1400&horizont=45,25,30,60&past_days=5&cellCoEff=-0.36&inverterEfficiency=0.8&albedo=0.25&timezone=Europe%2FBerlin&hourly=relativehumidity_2m%2Cwindspeed_10m") - forecast.update_ac_power_measurement(date_time=datetime.now(), ac_power_measurement=1000) +if __name__ == "__main__": + forecast = PVForecast( + prediction_hours=24, + url="https://api.akkudoktor.net/forecast?lat=50.8588&lon=7.3747&power=5000&azimuth=-10&tilt=7&powerInvertor=10000&horizont=20,27,22,20&power=4800&azimuth=-90&tilt=7&powerInvertor=10000&horizont=30,30,30,50&power=1400&azimuth=-40&tilt=60&powerInvertor=2000&horizont=60,30,0,30&power=1600&azimuth=5&tilt=45&powerInvertor=1400&horizont=45,25,30,60&past_days=5&cellCoEff=-0.36&inverterEfficiency=0.8&albedo=0.25&timezone=Europe%2FBerlin&hourly=relativehumidity_2m%2Cwindspeed_10m", + ) + forecast.update_ac_power_measurement( + date_time=datetime.now(), ac_power_measurement=1000 + ) forecast.print_ac_power_and_measurement() diff --git a/modules/class_soc_calc.py b/modules/class_soc_calc.py index 98f49dd..f88b7eb 100644 --- a/modules/class_soc_calc.py +++ b/modules/class_soc_calc.py @@ -1,19 +1,27 @@ +from datetime import datetime, timedelta + import mariadb -import pandas as pd -import numpy as np import matplotlib.pyplot as plt -from scipy.integrate import simpson -from datetime import datetime, timedelta +import numpy as np +import pandas as pd class BatteryDataProcessor: - def __init__(self, config, voltage_high_threshold, voltage_low_threshold, current_low_threshold, gap, battery_capacity_ah): + def __init__( + self, + config, + voltage_high_threshold, + voltage_low_threshold, + current_low_threshold, + gap, + battery_capacity_ah, + ): self.config = config self.voltage_high_threshold = voltage_high_threshold self.voltage_low_threshold = voltage_low_threshold self.current_low_threshold = current_low_threshold self.gap = gap - self.battery_capacity_ah = battery_capacity_ah + self.battery_capacity_ah = battery_capacity_ah self.conn = None self.data = None @@ -35,45 +43,55 @@ def fetch_data(self, start_time): """ self.cursor.execute(query, (start_time,)) rows = self.cursor.fetchall() - self.data = pd.DataFrame(rows, columns=['timestamp', 'data', 'topic']) - self.data['timestamp'] = pd.to_datetime(self.data['timestamp']) - self.data['data'] = self.data['data'].astype(float) + self.data = pd.DataFrame(rows, columns=["timestamp", "data", "topic"]) + self.data["timestamp"] = pd.to_datetime(self.data["timestamp"]) + self.data["data"] = self.data["data"].astype(float) def process_data(self): - self.data.drop_duplicates(subset=['timestamp', 'topic'], inplace=True) + self.data.drop_duplicates(subset=["timestamp", "topic"], inplace=True) - data_pivot = self.data.pivot(index='timestamp', columns='topic', values='data') - data_pivot = data_pivot.resample('1T').mean().interpolate() + data_pivot = self.data.pivot(index="timestamp", columns="topic", values="data") + data_pivot = data_pivot.resample("1T").mean().interpolate() data_pivot.columns.name = None data_pivot.reset_index(inplace=True) self.data = data_pivot def group_points(self, df): - df = df.sort_values('timestamp') + df = df.sort_values("timestamp") groups = [] group = [] last_time = None for _, row in df.iterrows(): - if last_time is None or (row['timestamp'] - last_time) <= pd.Timedelta(minutes=self.gap): + if last_time is None or (row["timestamp"] - last_time) <= pd.Timedelta( + minutes=self.gap + ): group.append(row) else: groups.append(group) group = [row] - last_time = row['timestamp'] + last_time = row["timestamp"] if group: groups.append(group) - + last_points = [group[-1] for group in groups] return last_points def find_soc_points(self): - condition_soc_100 = (self.data['battery_voltage'] >= self.voltage_high_threshold) & (self.data['battery_current'].abs() <= self.current_low_threshold) - condition_soc_0 = (self.data['battery_voltage'] <= self.voltage_low_threshold) & (self.data['battery_current'].abs() <= self.current_low_threshold) - - times_soc_100_all = self.data[condition_soc_100][['timestamp', 'battery_voltage', 'battery_current']] - times_soc_0_all = self.data[condition_soc_0][['timestamp', 'battery_voltage', 'battery_current']] + condition_soc_100 = ( + self.data["battery_voltage"] >= self.voltage_high_threshold + ) & (self.data["battery_current"].abs() <= self.current_low_threshold) + condition_soc_0 = ( + self.data["battery_voltage"] <= self.voltage_low_threshold + ) & (self.data["battery_current"].abs() <= self.current_low_threshold) + + times_soc_100_all = self.data[condition_soc_100][ + ["timestamp", "battery_voltage", "battery_current"] + ] + times_soc_0_all = self.data[condition_soc_0][ + ["timestamp", "battery_voltage", "battery_current"] + ] last_points_100 = self.group_points(times_soc_100_all) last_points_0 = self.group_points(times_soc_0_all) @@ -86,79 +104,100 @@ def find_soc_points(self): def calculate_resetting_soc(self, last_points_100_df, last_points_0_df): soc_values = [] integration_results = [] - reset_points = pd.concat([last_points_100_df, last_points_0_df]).sort_values('timestamp') + reset_points = pd.concat([last_points_100_df, last_points_0_df]).sort_values( + "timestamp" + ) # Initialisieren der SoC-Liste - self.data['calculated_soc'] = np.nan + self.data["calculated_soc"] = np.nan for i in range(len(reset_points)): start_point = reset_points.iloc[i] if i < len(reset_points) - 1: end_point = reset_points.iloc[i + 1] else: - end_point = self.data.iloc[-1] # Verwenden des letzten Datensatzes als Endpunkt + end_point = self.data.iloc[ + -1 + ] # Verwenden des letzten Datensatzes als Endpunkt - if start_point['timestamp'] in last_points_100_df['timestamp'].values: + if start_point["timestamp"] in last_points_100_df["timestamp"].values: initial_soc = 100 - elif start_point['timestamp'] in last_points_0_df['timestamp'].values: + elif start_point["timestamp"] in last_points_0_df["timestamp"].values: initial_soc = 0 - cut_data = self.data[(self.data['timestamp'] >= start_point['timestamp']) & (self.data['timestamp'] <= end_point['timestamp'])].copy() - cut_data['time_diff_hours'] = cut_data['timestamp'].diff().dt.total_seconds() / 3600 - cut_data.dropna(subset=['time_diff_hours'], inplace=True) + cut_data = self.data[ + (self.data["timestamp"] >= start_point["timestamp"]) + & (self.data["timestamp"] <= end_point["timestamp"]) + ].copy() + cut_data["time_diff_hours"] = ( + cut_data["timestamp"].diff().dt.total_seconds() / 3600 + ) + cut_data.dropna(subset=["time_diff_hours"], inplace=True) calculated_soc = initial_soc calculated_soc_list = [calculated_soc] integrated_current = 0 for j in range(1, len(cut_data)): - current = cut_data.iloc[j]['battery_current'] - delta_t = cut_data.iloc[j]['time_diff_hours'] - delta_soc = (current * delta_t) / self.battery_capacity_ah * 100 # Convert to percentage - + current = cut_data.iloc[j]["battery_current"] + delta_t = cut_data.iloc[j]["time_diff_hours"] + delta_soc = ( + (current * delta_t) / self.battery_capacity_ah * 100 + ) # Convert to percentage + calculated_soc += delta_soc calculated_soc = min(max(calculated_soc, 0), 100) # Clip to 0-100% calculated_soc_list.append(calculated_soc) - + # Integration des Stroms aufaddieren integrated_current += current * delta_t - cut_data['calculated_soc'] = calculated_soc_list - soc_values.append(cut_data[['timestamp', 'calculated_soc']]) - - integration_results.append({ - 'start_time': start_point['timestamp'], - 'end_time': end_point['timestamp'], - 'integrated_current': integrated_current, - 'start_soc': initial_soc, - 'end_soc': calculated_soc_list[-1] - }) - - soc_df = pd.concat(soc_values).drop_duplicates(subset=['timestamp']).reset_index(drop=True) + cut_data["calculated_soc"] = calculated_soc_list + soc_values.append(cut_data[["timestamp", "calculated_soc"]]) + + integration_results.append( + { + "start_time": start_point["timestamp"], + "end_time": end_point["timestamp"], + "integrated_current": integrated_current, + "start_soc": initial_soc, + "end_soc": calculated_soc_list[-1], + } + ) + + soc_df = ( + pd.concat(soc_values) + .drop_duplicates(subset=["timestamp"]) + .reset_index(drop=True) + ) return soc_df, integration_results - def calculate_soh(self, integration_results): soh_values = [] for result in integration_results: - delta_soc = abs(result['start_soc'] - result['end_soc']) # Use the actual change in SoC + delta_soc = abs( + result["start_soc"] - result["end_soc"] + ) # Use the actual change in SoC if delta_soc > 0: # Avoid division by zero - effective_capacity_ah = result['integrated_current'] + effective_capacity_ah = result["integrated_current"] soh = (effective_capacity_ah / self.battery_capacity_ah) * 100 - soh_values.append({'timestamp': result['end_time'], 'soh': soh}) + soh_values.append({"timestamp": result["end_time"], "soh": soh}) soh_df = pd.DataFrame(soh_values) return soh_df - def delete_existing_soc_entries(self, soc_df): delete_query = """ DELETE FROM pip WHERE timestamp = %s AND topic = 'calculated_soc' """ - timestamps = [(row['timestamp'].strftime('%Y-%m-%d %H:%M:%S'),) for _, row in soc_df.iterrows() if pd.notna(row['timestamp'])] - + timestamps = [ + (row["timestamp"].strftime("%Y-%m-%d %H:%M:%S"),) + for _, row in soc_df.iterrows() + if pd.notna(row["timestamp"]) + ] + self.cursor.executemany(delete_query, timestamps) self.conn.commit() @@ -167,9 +206,9 @@ def update_database_with_soc(self, soc_df): self.delete_existing_soc_entries(soc_df) # Resample `soc_df` auf 5-Minuten-Intervalle und berechnen des Mittelwerts - soc_df.set_index('timestamp', inplace=True) - soc_df_resampled = soc_df.resample('5T').mean().dropna().reset_index() - #soc_df_resampled['timestamp'] = soc_df_resampled['timestamp'].apply(lambda x: x.strftime('%Y-%m-%d %H:%M:%S')) + soc_df.set_index("timestamp", inplace=True) + soc_df_resampled = soc_df.resample("5T").mean().dropna().reset_index() + # soc_df_resampled['timestamp'] = soc_df_resampled['timestamp'].apply(lambda x: x.strftime('%Y-%m-%d %H:%M:%S')) print(soc_df_resampled) # Einfügen der berechneten SoC-Werte in die Datenbank @@ -179,8 +218,11 @@ def update_database_with_soc(self, soc_df): """ for _, row in soc_df_resampled.iterrows(): print(row) - print(row['timestamp']) - record = (row['timestamp'].strftime('%Y-%m-%d %H:%M:%S'), row['calculated_soc']) + print(row["timestamp"]) + record = ( + row["timestamp"].strftime("%Y-%m-%d %H:%M:%S"), + row["calculated_soc"], + ) try: self.cursor.execute(insert_query, record) except mariadb.OperationalError as e: @@ -188,38 +230,57 @@ def update_database_with_soc(self, soc_df): self.conn.commit() - - - - - def plot_data(self, last_points_100_df, last_points_0_df, soc_df): plt.figure(figsize=(14, 10)) plt.subplot(4, 1, 1) - plt.plot(self.data['timestamp'], self.data['battery_voltage'], label='Battery Voltage', color='blue') - plt.scatter(last_points_100_df['timestamp'], last_points_100_df['battery_voltage'], color='green', marker='o', label='100% SoC Points') - #plt.scatter(last_points_0_df['timestamp'], last_points_0_df['battery_voltage'], color='red', marker='x', label='0% SoC Points') - plt.xlabel('Timestamp') - plt.ylabel('Voltage (V)') + plt.plot( + self.data["timestamp"], + self.data["battery_voltage"], + label="Battery Voltage", + color="blue", + ) + plt.scatter( + last_points_100_df["timestamp"], + last_points_100_df["battery_voltage"], + color="green", + marker="o", + label="100% SoC Points", + ) + # plt.scatter(last_points_0_df['timestamp'], last_points_0_df['battery_voltage'], color='red', marker='x', label='0% SoC Points') + plt.xlabel("Timestamp") + plt.ylabel("Voltage (V)") plt.legend() - plt.title('Battery Voltage over Time') + plt.title("Battery Voltage over Time") plt.subplot(4, 1, 2) - plt.plot(self.data['timestamp'], self.data['battery_current'], label='Battery Current', color='orange') - plt.scatter(last_points_100_df['timestamp'], last_points_100_df['battery_current'], color='green', marker='o', label='100% SoC Points') - #plt.scatter(last_points_0_df['timestamp'], last_points_0_df['battery_current'], color='red', marker='x', label='0% SoC Points') - plt.xlabel('Timestamp') - plt.ylabel('Current (A)') + plt.plot( + self.data["timestamp"], + self.data["battery_current"], + label="Battery Current", + color="orange", + ) + plt.scatter( + last_points_100_df["timestamp"], + last_points_100_df["battery_current"], + color="green", + marker="o", + label="100% SoC Points", + ) + # plt.scatter(last_points_0_df['timestamp'], last_points_0_df['battery_current'], color='red', marker='x', label='0% SoC Points') + plt.xlabel("Timestamp") + plt.ylabel("Current (A)") plt.legend() - plt.title('Battery Current over Time') + plt.title("Battery Current over Time") plt.subplot(4, 1, 3) - plt.plot(soc_df['timestamp'], soc_df['calculated_soc'], label='SoC', color='purple') - plt.xlabel('Timestamp') - plt.ylabel('SoC (%)') + plt.plot( + soc_df["timestamp"], soc_df["calculated_soc"], label="SoC", color="purple" + ) + plt.xlabel("Timestamp") + plt.ylabel("SoC (%)") plt.legend() - plt.title('State of Charge (SoC) over Time') + plt.title("State of Charge (SoC) over Time") # plt.subplot(4, 1, 4) # plt.plot(soh_df['timestamp'], soh_df['soh'], label='SoH', color='brown') @@ -228,44 +289,42 @@ def plot_data(self, last_points_100_df, last_points_0_df, soc_df): # plt.legend() # plt.title('State of Health (SoH) over Time') - - plt.tight_layout() plt.show() -if __name__ == '__main__': +if __name__ == "__main__": # MariaDB Verbindungsdetails - # Parameter festlegen voltage_high_threshold = 55.4 # 100% SoC voltage_low_threshold = 46.5 # 0% SoC current_low_threshold = 2 # Niedriger Strom für beide Zustände gap = 30 # Zeitlücke in Minuten zum Gruppieren von Maxima/Minima - bat_capacity = 33 * 1000 / 48 + bat_capacity = 33 * 1000 / 48 # Zeitpunkt X definieren - zeitpunkt_x = (datetime.now() - timedelta(weeks=100)).strftime('%Y-%m-%d %H:%M:%S') - + zeitpunkt_x = (datetime.now() - timedelta(weeks=100)).strftime("%Y-%m-%d %H:%M:%S") # BatteryDataProcessor instanziieren und verwenden - processor = BatteryDataProcessor(config, voltage_high_threshold, voltage_low_threshold, current_low_threshold, gap,bat_capacity) + processor = BatteryDataProcessor( + config, + voltage_high_threshold, + voltage_low_threshold, + current_low_threshold, + gap, + bat_capacity, + ) processor.connect_db() processor.fetch_data(zeitpunkt_x) processor.process_data() last_points_100_df, last_points_0_df = processor.find_soc_points() - soc_df, integration_results = processor.calculate_resetting_soc(last_points_100_df, last_points_0_df) - #soh_df = processor.calculate_soh(integration_results) + soc_df, integration_results = processor.calculate_resetting_soc( + last_points_100_df, last_points_0_df + ) + # soh_df = processor.calculate_soh(integration_results) processor.update_database_with_soc(soc_df) - processor.plot_data(last_points_100_df, last_points_0_df, soc_df) - - - - processor.disconnect_db() - - diff --git a/modules/class_sommerzeit.py b/modules/class_sommerzeit.py index e9bc032..4e2a9e8 100644 --- a/modules/class_sommerzeit.py +++ b/modules/class_sommerzeit.py @@ -1,6 +1,8 @@ import datetime + import pytz + def ist_dst_wechsel(tag, timezone="Europe/Berlin"): """Checks if Daylight Saving Time (DST) starts or ends on a given day.""" tz = pytz.timezone(timezone) @@ -17,9 +19,10 @@ def ist_dst_wechsel(tag, timezone="Europe/Berlin"): return dst_change + # # Example usage # start_date = datetime.datetime(2024, 3, 31) # Date of the DST change # if ist_dst_wechsel(start_date): - # prediction_hours = 23 # Adjust to 23 hours for DST change days +# prediction_hours = 23 # Adjust to 23 hours for DST change days # else: - # prediction_hours = 24 # Default value for days without DST change +# prediction_hours = 24 # Default value for days without DST change diff --git a/modules/class_strompreis.py b/modules/class_strompreis.py index 46bbd94..bd540dc 100644 --- a/modules/class_strompreis.py +++ b/modules/class_strompreis.py @@ -1,23 +1,27 @@ +import hashlib import json import os -import hashlib -import requests from datetime import datetime, timedelta + import numpy as np import pytz +import requests # Example: Converting a UTC timestamp to local time -utc_time = datetime.strptime('2024-03-28T01:00:00.000Z', '%Y-%m-%dT%H:%M:%S.%fZ') +utc_time = datetime.strptime("2024-03-28T01:00:00.000Z", "%Y-%m-%dT%H:%M:%S.%fZ") utc_time = utc_time.replace(tzinfo=pytz.utc) # Replace 'Europe/Berlin' with your own timezone -local_time = utc_time.astimezone(pytz.timezone('Europe/Berlin')) +local_time = utc_time.astimezone(pytz.timezone("Europe/Berlin")) print(local_time) + def repeat_to_shape(array, target_shape): # Check if the array fits the target shape if len(target_shape) != array.ndim: - raise ValueError("Array and target shape must have the same number of dimensions") + raise ValueError( + "Array and target shape must have the same number of dimensions" + ) # Number of repetitions per dimension repeats = tuple(target_shape[i] // array.shape[i] for i in range(array.ndim)) @@ -26,103 +30,112 @@ def repeat_to_shape(array, target_shape): expanded_array = np.tile(array, repeats) return expanded_array + class HourlyElectricityPriceForecast: - def __init__(self, source, cache_dir='cache', charges=0.000228, prediction_hours=24): # 228 + def __init__( + self, source, cache_dir="cache", charges=0.000228, prediction_hours=24 + ): # 228 self.cache_dir = cache_dir os.makedirs(self.cache_dir, exist_ok=True) - self.cache_time_file = os.path.join(self.cache_dir, 'cache_timestamp.txt') + self.cache_time_file = os.path.join(self.cache_dir, "cache_timestamp.txt") self.prices = self.load_data(source) self.charges = charges self.prediction_hours = prediction_hours - + def load_data(self, source): cache_filename = self.get_cache_filename(source) - if source.startswith('http'): + if source.startswith("http"): if os.path.exists(cache_filename) and not self.is_cache_expired(): print("Loading data from cache...") - with open(cache_filename, 'r') as file: + with open(cache_filename, "r") as file: json_data = json.load(file) else: print("Loading data from the URL...") response = requests.get(source) if response.status_code == 200: json_data = response.json() - with open(cache_filename, 'w') as file: + with open(cache_filename, "w") as file: json.dump(json_data, file) self.update_cache_timestamp() else: raise Exception(f"Error fetching data: {response.status_code}") else: - with open(source, 'r') as file: + with open(source, "r") as file: json_data = json.load(file) - return json_data['values'] - + return json_data["values"] + def get_cache_filename(self, url): hash_object = hashlib.sha256(url.encode()) hex_dig = hash_object.hexdigest() return os.path.join(self.cache_dir, f"cache_{hex_dig}.json") - + def is_cache_expired(self): if not os.path.exists(self.cache_time_file): return True - with open(self.cache_time_file, 'r') as file: + with open(self.cache_time_file, "r") as file: timestamp_str = file.read() - last_cache_time = datetime.strptime(timestamp_str, '%Y-%m-%d %H:%M:%S') + last_cache_time = datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S") return datetime.now() - last_cache_time > timedelta(hours=1) - + def update_cache_timestamp(self): - with open(self.cache_time_file, 'w') as file: - file.write(datetime.now().strftime('%Y-%m-%d %H:%M:%S')) + with open(self.cache_time_file, "w") as file: + file.write(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) def get_price_for_date(self, date_str): """Returns all prices for the specified date, including the price from 00:00 of the previous day.""" # Convert date string to datetime object - date_obj = datetime.strptime(date_str, '%Y-%m-%d') - + date_obj = datetime.strptime(date_str, "%Y-%m-%d") + # Calculate the previous day previous_day = date_obj - timedelta(days=1) - previous_day_str = previous_day.strftime('%Y-%m-%d') - + previous_day_str = previous_day.strftime("%Y-%m-%d") + # Extract the price from 00:00 of the previous day last_price_of_previous_day = [ entry["marketpriceEurocentPerKWh"] + self.charges - for entry in self.prices if previous_day_str in entry['end'] + for entry in self.prices + if previous_day_str in entry["end"] ][-1] - + # Extract all prices for the specified date date_prices = [ entry["marketpriceEurocentPerKWh"] + self.charges - for entry in self.prices if date_str in entry['end'] + for entry in self.prices + if date_str in entry["end"] ] print(f"getPrice: {len(date_prices)}") - + # Add the last price of the previous day at the start of the list if len(date_prices) == 23: date_prices.insert(0, last_price_of_previous_day) return np.array(date_prices) / (1000.0 * 100.0) + self.charges - + def get_price_for_daterange(self, start_date_str, end_date_str): """Returns all prices between the start and end dates.""" print(start_date_str) print(end_date_str) - start_date_utc = datetime.strptime(start_date_str, "%Y-%m-%d").replace(tzinfo=pytz.utc) - end_date_utc = datetime.strptime(end_date_str, "%Y-%m-%d").replace(tzinfo=pytz.utc) - start_date = start_date_utc.astimezone(pytz.timezone('Europe/Berlin')) - end_date = end_date_utc.astimezone(pytz.timezone('Europe/Berlin')) + start_date_utc = datetime.strptime(start_date_str, "%Y-%m-%d").replace( + tzinfo=pytz.utc + ) + end_date_utc = datetime.strptime(end_date_str, "%Y-%m-%d").replace( + tzinfo=pytz.utc + ) + start_date = start_date_utc.astimezone(pytz.timezone("Europe/Berlin")) + end_date = end_date_utc.astimezone(pytz.timezone("Europe/Berlin")) price_list = [] while start_date < end_date: date_str = start_date.strftime("%Y-%m-%d") daily_prices = self.get_price_for_date(date_str) - + if daily_prices.size == 24: price_list.extend(daily_prices) start_date += timedelta(days=1) - + # If prediction hours are greater than 0, reshape the price list if self.prediction_hours > 0: price_list = repeat_to_shape(np.array(price_list), (self.prediction_hours,)) - + return price_list diff --git a/modules/visualize.py b/modules/visualize.py index 261cfb6..6e19614 100644 --- a/modules/visualize.py +++ b/modules/visualize.py @@ -1,72 +1,103 @@ -import numpy as np -import matplotlib.pyplot as plt -from matplotlib.backends.backend_pdf import PdfPages from datetime import datetime -from modules.class_sommerzeit import * # Ensure this matches the actual import path -from modules.class_load_container import Gesamtlast # Ensure this matches the actual import path # Set the backend for matplotlib to Agg import matplotlib -matplotlib.use('Agg') +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.backends.backend_pdf import PdfPages + +from modules.class_sommerzeit import * # Ensure this matches the actual import path -def visualisiere_ergebnisse(gesamtlast, pv_forecast, strompreise, ergebnisse, discharge_hours, laden_moeglich, temperature, start_hour, prediction_hours, einspeiseverguetung_euro_pro_wh, filename="visualization_results.pdf", extra_data=None): - +matplotlib.use("Agg") + + +def visualisiere_ergebnisse( + gesamtlast, + pv_forecast, + strompreise, + ergebnisse, + discharge_hours, + laden_moeglich, + temperature, + start_hour, + prediction_hours, + einspeiseverguetung_euro_pro_wh, + filename="visualization_results.pdf", + extra_data=None, +): ##################### # 24-hour visualization ##################### with PdfPages(filename) as pdf: - # Load and PV generation plt.figure(figsize=(14, 14)) plt.subplot(3, 3, 1) hours = np.arange(0, prediction_hours) - + gesamtlast_array = np.array(gesamtlast) # Plot individual loads - plt.plot(hours, gesamtlast_array, label='Load (Wh)', marker='o') - + plt.plot(hours, gesamtlast_array, label="Load (Wh)", marker="o") + # Calculate and plot total load - plt.plot(hours, gesamtlast_array, label='Total Load (Wh)', marker='o', linewidth=2, linestyle='--') - plt.xlabel('Hour') - plt.ylabel('Load (Wh)') - plt.title('Load Profiles') + plt.plot( + hours, + gesamtlast_array, + label="Total Load (Wh)", + marker="o", + linewidth=2, + linestyle="--", + ) + plt.xlabel("Hour") + plt.ylabel("Load (Wh)") + plt.title("Load Profiles") plt.grid(True) plt.legend() # Electricity prices hours_p = np.arange(0, len(strompreise)) plt.subplot(3, 2, 2) - plt.plot(hours_p, strompreise, label='Electricity Price (€/Wh)', color='purple', marker='s') - plt.title('Electricity Prices') - plt.xlabel('Hour of the Day') - plt.ylabel('Price (€/Wh)') + plt.plot( + hours_p, + strompreise, + label="Electricity Price (€/Wh)", + color="purple", + marker="s", + ) + plt.title("Electricity Prices") + plt.xlabel("Hour of the Day") + plt.ylabel("Price (€/Wh)") plt.legend() plt.grid(True) # PV forecast plt.subplot(3, 2, 3) - plt.plot(hours, pv_forecast, label='PV Generation (Wh)', marker='x') - plt.title('PV Forecast') - plt.xlabel('Hour of the Day') - plt.ylabel('Wh') + plt.plot(hours, pv_forecast, label="PV Generation (Wh)", marker="x") + plt.title("PV Forecast") + plt.xlabel("Hour of the Day") + plt.ylabel("Wh") plt.legend() plt.grid(True) # Feed-in remuneration plt.subplot(3, 2, 4) - plt.plot(hours, einspeiseverguetung_euro_pro_wh, label='Remuneration (€/Wh)', marker='x') - plt.title('Remuneration') - plt.xlabel('Hour of the Day') - plt.ylabel('€/Wh') + plt.plot( + hours, + einspeiseverguetung_euro_pro_wh, + label="Remuneration (€/Wh)", + marker="x", + ) + plt.title("Remuneration") + plt.xlabel("Hour of the Day") + plt.ylabel("€/Wh") plt.legend() plt.grid(True) # Temperature forecast plt.subplot(3, 2, 5) - plt.title('Temperature Forecast (°C)') - plt.plot(hours, temperature, label='Temperature (°C)', marker='x') - plt.xlabel('Hour of the Day') - plt.ylabel('°C') + plt.title("Temperature Forecast (°C)") + plt.plot(hours, temperature, label="Temperature (°C)", marker="x") + plt.xlabel("Hour of the Day") + plt.ylabel("°C") plt.legend() plt.grid(True) @@ -76,73 +107,125 @@ def visualisiere_ergebnisse(gesamtlast, pv_forecast, strompreise, ergebnisse, di ##################### # Start hour visualization ##################### - + plt.figure(figsize=(14, 10)) - + if ist_dst_wechsel(datetime.datetime.now()): hours = np.arange(start_hour, prediction_hours - 1) else: hours = np.arange(start_hour, prediction_hours) - + # Energy flow, grid feed-in, and grid consumption plt.subplot(3, 2, 1) - plt.plot(hours, ergebnisse['Last_Wh_pro_Stunde'], label='Load (Wh)', marker='o') - plt.plot(hours, ergebnisse['Haushaltsgeraet_wh_pro_stunde'], label='Household Device (Wh)', marker='o') - plt.plot(hours, ergebnisse['Netzeinspeisung_Wh_pro_Stunde'], label='Grid Feed-in (Wh)', marker='x') - plt.plot(hours, ergebnisse['Netzbezug_Wh_pro_Stunde'], label='Grid Consumption (Wh)', marker='^') - plt.plot(hours, ergebnisse['Verluste_Pro_Stunde'], label='Losses (Wh)', marker='^') - plt.title('Energy Flow per Hour') - plt.xlabel('Hour') - plt.ylabel('Energy (Wh)') + plt.plot(hours, ergebnisse["Last_Wh_pro_Stunde"], label="Load (Wh)", marker="o") + plt.plot( + hours, + ergebnisse["Haushaltsgeraet_wh_pro_stunde"], + label="Household Device (Wh)", + marker="o", + ) + plt.plot( + hours, + ergebnisse["Netzeinspeisung_Wh_pro_Stunde"], + label="Grid Feed-in (Wh)", + marker="x", + ) + plt.plot( + hours, + ergebnisse["Netzbezug_Wh_pro_Stunde"], + label="Grid Consumption (Wh)", + marker="^", + ) + plt.plot( + hours, ergebnisse["Verluste_Pro_Stunde"], label="Losses (Wh)", marker="^" + ) + plt.title("Energy Flow per Hour") + plt.xlabel("Hour") + plt.ylabel("Energy (Wh)") plt.legend() # State of charge for batteries plt.subplot(3, 2, 2) - plt.plot(hours, ergebnisse['akku_soc_pro_stunde'], label='PV Battery (%)', marker='x') - plt.plot(hours, ergebnisse['E-Auto_SoC_pro_Stunde'], label='E-Car Battery (%)', marker='x') - plt.legend(loc='upper left', bbox_to_anchor=(1, 1)) # Place legend outside the plot - plt.grid(True, which='both', axis='x') # Grid for every hour + plt.plot( + hours, ergebnisse["akku_soc_pro_stunde"], label="PV Battery (%)", marker="x" + ) + plt.plot( + hours, + ergebnisse["E-Auto_SoC_pro_Stunde"], + label="E-Car Battery (%)", + marker="x", + ) + plt.legend( + loc="upper left", bbox_to_anchor=(1, 1) + ) # Place legend outside the plot + plt.grid(True, which="both", axis="x") # Grid for every hour ax1 = plt.subplot(3, 2, 3) for hour, value in enumerate(discharge_hours): - ax1.axvspan(hour, hour + 1, color='red', ymax=value, alpha=0.3, label='Discharge Possibility' if hour == 0 else "") + ax1.axvspan( + hour, + hour + 1, + color="red", + ymax=value, + alpha=0.3, + label="Discharge Possibility" if hour == 0 else "", + ) for hour, value in enumerate(laden_moeglich): - ax1.axvspan(hour, hour + 1, color='green', ymax=value, alpha=0.3, label='Charging Possibility' if hour == 0 else "") - ax1.legend(loc='upper left') + ax1.axvspan( + hour, + hour + 1, + color="green", + ymax=value, + alpha=0.3, + label="Charging Possibility" if hour == 0 else "", + ) + ax1.legend(loc="upper left") ax1.set_xlim(0, prediction_hours) pdf.savefig() # Save the current figure state to the PDF plt.close() # Close the current figure to free up memory - + # Financial overview fig, axs = plt.subplots(1, 2, figsize=(14, 10)) # Create a 1x2 grid of subplots - total_costs = ergebnisse['Gesamtkosten_Euro'] - total_revenue = ergebnisse['Gesamteinnahmen_Euro'] - total_balance = ergebnisse['Gesamtbilanz_Euro'] - losses = ergebnisse['Gesamt_Verluste'] + total_costs = ergebnisse["Gesamtkosten_Euro"] + total_revenue = ergebnisse["Gesamteinnahmen_Euro"] + total_balance = ergebnisse["Gesamtbilanz_Euro"] + losses = ergebnisse["Gesamt_Verluste"] # Costs and revenues per hour on the first axis (axs[0]) - axs[0].plot(hours, ergebnisse['Kosten_Euro_pro_Stunde'], label='Costs (Euro)', marker='o', color='red') - axs[0].plot(hours, ergebnisse['Einnahmen_Euro_pro_Stunde'], label='Revenue (Euro)', marker='x', color='green') - axs[0].set_title('Financial Balance per Hour') - axs[0].set_xlabel('Hour') - axs[0].set_ylabel('Euro') + axs[0].plot( + hours, + ergebnisse["Kosten_Euro_pro_Stunde"], + label="Costs (Euro)", + marker="o", + color="red", + ) + axs[0].plot( + hours, + ergebnisse["Einnahmen_Euro_pro_Stunde"], + label="Revenue (Euro)", + marker="x", + color="green", + ) + axs[0].set_title("Financial Balance per Hour") + axs[0].set_xlabel("Hour") + axs[0].set_ylabel("Euro") axs[0].legend() axs[0].grid(True) # Summary of finances on the second axis (axs[1]) - labels = ['Total Costs [€]', 'Total Revenue [€]', 'Total Balance [€]'] + labels = ["Total Costs [€]", "Total Revenue [€]", "Total Balance [€]"] values = [total_costs, total_revenue, total_balance] - colors = ['red' if value > 0 else 'green' for value in values] + colors = ["red" if value > 0 else "green" for value in values] axs[1].bar(labels, values, color=colors) - axs[1].set_title('Financial Overview') - axs[1].set_ylabel('Euro') + axs[1].set_title("Financial Overview") + axs[1].set_ylabel("Euro") # Second axis (ax2) for losses, shared with axs[1] ax2 = axs[1].twinx() - ax2.bar('Total Losses', losses, color='blue') - ax2.set_ylabel('Losses [Wh]', color='blue') - ax2.tick_params(axis='y', labelcolor='blue') + ax2.bar("Total Losses", losses, color="blue") + ax2.set_ylabel("Losses [Wh]", color="blue") + ax2.tick_params(axis="y", labelcolor="blue") pdf.savefig() # Save the complete figure to the PDF plt.close() # Close the figure @@ -154,17 +237,31 @@ def visualisiere_ergebnisse(gesamtlast, pv_forecast, strompreise, ergebnisse, di f1 = np.array(extra_data["verluste"]) f2 = np.array(extra_data["bilanz"]) n1 = np.array(extra_data["nebenbedingung"]) - scatter = plt.scatter(f1, f2, c=n1, cmap='viridis') + scatter = plt.scatter(f1, f2, c=n1, cmap="viridis") # Add color legend - plt.colorbar(scatter, label='Constraint') + plt.colorbar(scatter, label="Constraint") pdf.savefig() # Save the complete figure to the PDF plt.close() # Close the figure plt.figure(figsize=(14, 10)) - filtered_losses = np.array([v for v, n in zip(extra_data["verluste"], extra_data["nebenbedingung"]) if n < 0.01]) - filtered_balance = np.array([b for b, n in zip(extra_data["bilanz"], extra_data["nebenbedingung"]) if n < 0.01]) + filtered_losses = np.array( + [ + v + for v, n in zip( + extra_data["verluste"], extra_data["nebenbedingung"] + ) + if n < 0.01 + ] + ) + filtered_balance = np.array( + [ + b + for b, n in zip(extra_data["bilanz"], extra_data["nebenbedingung"]) + if n < 0.01 + ] + ) best_loss = min(filtered_losses) worst_loss = max(filtered_losses) @@ -172,19 +269,21 @@ def visualisiere_ergebnisse(gesamtlast, pv_forecast, strompreise, ergebnisse, di worst_balance = max(filtered_balance) data = [filtered_losses, filtered_balance] - labels = ['Losses', 'Balance'] + labels = ["Losses", "Balance"] # Create plots - fig, axs = plt.subplots(1, 2, figsize=(10, 6), sharey=False) # Two subplots, separate y-axes + fig, axs = plt.subplots( + 1, 2, figsize=(10, 6), sharey=False + ) # Two subplots, separate y-axes # First violin plot for losses axs[0].violinplot(data[0], showmeans=True, showmedians=True) - axs[0].set_title('Losses') - axs[0].set_xticklabels(['Losses']) + axs[0].set_title("Losses") + axs[0].set_xticklabels(["Losses"]) # Second violin plot for balance axs[1].violinplot(data[1], showmeans=True, showmedians=True) - axs[1].set_title('Balance') - axs[1].set_xticklabels(['Balance']) + axs[1].set_title("Balance") + axs[1].set_xticklabels(["Balance"]) # Fine-tuning plt.tight_layout() diff --git a/server_load_profile.py b/server_load_profile.py index d276c89..996e9eb 100644 --- a/server_load_profile.py +++ b/server_load_profile.py @@ -1,35 +1,43 @@ -from flask import Flask, jsonify, request -import numpy as np from datetime import datetime -import modules.class_load as cl from pprint import pprint +from flask import Flask, jsonify, request + +import modules.class_load as cl + app = Flask(__name__) # Constants -DATE_FORMAT = '%Y-%m-%d' +DATE_FORMAT = "%Y-%m-%d" EXPECTED_ARRAY_SHAPE = (2, 24) -FILEPATH = r'.\load_profiles.npz' +FILEPATH = r".\load_profiles.npz" + def get_load_forecast(year_energy): """Initialize LoadForecast with the given year_energy.""" return cl.LoadForecast(filepath=FILEPATH, year_energy=float(year_energy)) + def validate_date(date_str): """Validate the date string and return a datetime object.""" try: return datetime.strptime(date_str, DATE_FORMAT) except ValueError: - raise ValueError("Date is not in the correct format. Expected format: YYYY-MM-DD.") + raise ValueError( + "Date is not in the correct format. Expected format: YYYY-MM-DD." + ) -@app.route('/getdata', methods=['GET']) + +@app.route("/getdata", methods=["GET"]) def get_data(): # Retrieve the date and year_energy from query parameters - date_str = request.args.get('date') - year_energy = request.args.get('year_energy') + date_str = request.args.get("date") + year_energy = request.args.get("year_energy") if not date_str or not year_energy: - return jsonify({"error": "Missing 'date' or 'year_energy' query parameter."}), 400 + return jsonify( + {"error": "Missing 'date' or 'year_energy' query parameter."} + ), 400 try: # Validate and convert the date @@ -54,5 +62,6 @@ def get_data(): # Return a generic error message for unexpected errors return jsonify({"error": "An unexpected error occurred."}), 500 -if __name__ == '__main__': + +if __name__ == "__main__": app.run(debug=True) diff --git a/test.py b/test.py index 13fd243..f62d901 100644 --- a/test.py +++ b/test.py @@ -1,10 +1,6 @@ #!/usr/bin/env python3 -import numpy as np -from datetime import datetime from pprint import pprint -import matplotlib.pyplot as plt -from deap import base, creator, tools, algorithms # Import necessary modules from the project from modules.class_optimize import optimization_problem @@ -14,75 +10,355 @@ # PV Forecast (in W) pv_forecast = [ - 0, 0, 0, 0, 0, 0, 0, 8.05, 352.91, 728.51, 930.28, 1043.25, - 1106.74, 1161.69, 6018.82, 5519.07, 3969.88, 3017.96, 1943.07, - 1007.17, 319.67, 7.88, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5.04, 335.59, - 705.32, 1121.12, 1604.79, 2157.38, 1433.25, 5718.49, 4553.96, - 3027.55, 2574.46, 1720.4, 963.4, 383.3, 0, 0, 0 + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 8.05, + 352.91, + 728.51, + 930.28, + 1043.25, + 1106.74, + 1161.69, + 6018.82, + 5519.07, + 3969.88, + 3017.96, + 1943.07, + 1007.17, + 319.67, + 7.88, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 5.04, + 335.59, + 705.32, + 1121.12, + 1604.79, + 2157.38, + 1433.25, + 5718.49, + 4553.96, + 3027.55, + 2574.46, + 1720.4, + 963.4, + 383.3, + 0, + 0, + 0, ] # Temperature Forecast (in degree C) temperature_forecast = [ - 18.3, 17.8, 16.9, 16.2, 15.6, 15.1, 14.6, 14.2, 14.3, 14.8, 15.7, - 16.7, 17.4, 18.0, 18.6, 19.2, 19.1, 18.7, 18.5, 17.7, 16.2, 14.6, - 13.6, 13.0, 12.6, 12.2, 11.7, 11.6, 11.3, 11.0, 10.7, 10.2, 11.4, - 14.4, 16.4, 18.3, 19.5, 20.7, 21.9, 22.7, 23.1, 23.1, 22.8, 21.8, - 20.2, 19.1, 18.0, 17.4 + 18.3, + 17.8, + 16.9, + 16.2, + 15.6, + 15.1, + 14.6, + 14.2, + 14.3, + 14.8, + 15.7, + 16.7, + 17.4, + 18.0, + 18.6, + 19.2, + 19.1, + 18.7, + 18.5, + 17.7, + 16.2, + 14.6, + 13.6, + 13.0, + 12.6, + 12.2, + 11.7, + 11.6, + 11.3, + 11.0, + 10.7, + 10.2, + 11.4, + 14.4, + 16.4, + 18.3, + 19.5, + 20.7, + 21.9, + 22.7, + 23.1, + 23.1, + 22.8, + 21.8, + 20.2, + 19.1, + 18.0, + 17.4, ] # Electricity Price (in Euro per Wh) strompreis_euro_pro_wh = [ - 0.0003384, 0.0003318, 0.0003284, 0.0003283, 0.0003289, 0.0003334, - 0.0003290, 0.0003302, 0.0003042, 0.0002430, 0.0002280, 0.0002212, - 0.0002093, 0.0001879, 0.0001838, 0.0002004, 0.0002198, 0.0002270, - 0.0002997, 0.0003195, 0.0003081, 0.0002969, 0.0002921, 0.0002780, - 0.0003384, 0.0003318, 0.0003284, 0.0003283, 0.0003289, 0.0003334, - 0.0003290, 0.0003302, 0.0003042, 0.0002430, 0.0002280, 0.0002212, - 0.0002093, 0.0001879, 0.0001838, 0.0002004, 0.0002198, 0.0002270, - 0.0002997, 0.0003195, 0.0003081, 0.0002969, 0.0002921, 0.0002780 + 0.0003384, + 0.0003318, + 0.0003284, + 0.0003283, + 0.0003289, + 0.0003334, + 0.0003290, + 0.0003302, + 0.0003042, + 0.0002430, + 0.0002280, + 0.0002212, + 0.0002093, + 0.0001879, + 0.0001838, + 0.0002004, + 0.0002198, + 0.0002270, + 0.0002997, + 0.0003195, + 0.0003081, + 0.0002969, + 0.0002921, + 0.0002780, + 0.0003384, + 0.0003318, + 0.0003284, + 0.0003283, + 0.0003289, + 0.0003334, + 0.0003290, + 0.0003302, + 0.0003042, + 0.0002430, + 0.0002280, + 0.0002212, + 0.0002093, + 0.0001879, + 0.0001838, + 0.0002004, + 0.0002198, + 0.0002270, + 0.0002997, + 0.0003195, + 0.0003081, + 0.0002969, + 0.0002921, + 0.0002780, ] # Overall System Load (in W) gesamtlast = [ - 676.71, 876.19, 527.13, 468.88, 531.38, 517.95, 483.15, 472.28, - 1011.68, 995.00, 1053.07, 1063.91, 1320.56, 1132.03, 1163.67, - 1176.82, 1216.22, 1103.78, 1129.12, 1178.71, 1050.98, 988.56, - 912.38, 704.61, 516.37, 868.05, 694.34, 608.79, 556.31, 488.89, - 506.91, 804.89, 1141.98, 1056.97, 992.46, 1155.99, 827.01, - 1257.98, 1232.67, 871.26, 860.88, 1158.03, 1222.72, 1221.04, - 949.99, 987.01, 733.99, 592.97 + 676.71, + 876.19, + 527.13, + 468.88, + 531.38, + 517.95, + 483.15, + 472.28, + 1011.68, + 995.00, + 1053.07, + 1063.91, + 1320.56, + 1132.03, + 1163.67, + 1176.82, + 1216.22, + 1103.78, + 1129.12, + 1178.71, + 1050.98, + 988.56, + 912.38, + 704.61, + 516.37, + 868.05, + 694.34, + 608.79, + 556.31, + 488.89, + 506.91, + 804.89, + 1141.98, + 1056.97, + 992.46, + 1155.99, + 827.01, + 1257.98, + 1232.67, + 871.26, + 860.88, + 1158.03, + 1222.72, + 1221.04, + 949.99, + 987.01, + 733.99, + 592.97, ] # Start Solution (binary) start_solution = [ - 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, - 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, - 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, - 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, ] # Define parameters for the optimization problem parameter = { - "preis_euro_pro_wh_akku": 10e-05, # Cost of storing energy in battery (per Wh) - 'pv_soc': 80, # Initial state of charge (SOC) of PV battery (%) - 'pv_akku_cap': 26400, # Battery capacity (in Wh) - 'year_energy': 4100000, # Yearly energy consumption (in Wh) - 'einspeiseverguetung_euro_pro_wh': 7e-05, # Feed-in tariff for exporting electricity (per Wh) - 'max_heizleistung': 1000, # Maximum heating power (in W) - "gesamtlast": gesamtlast, # Overall load on the system - 'pv_forecast': pv_forecast, # PV generation forecast (48 hours) - "temperature_forecast": temperature_forecast, # Temperature forecast (48 hours) - "strompreis_euro_pro_wh": strompreis_euro_pro_wh, # Electricity price forecast (48 hours) - 'eauto_min_soc': 0, # Minimum SOC for electric car - 'eauto_cap': 60000, # Electric car battery capacity (Wh) - 'eauto_charge_efficiency': 0.95, # Charging efficiency of the electric car - 'eauto_charge_power': 11040, # Charging power of the electric car (W) - 'eauto_soc': 54, # Current SOC of the electric car (%) - 'pvpowernow': 211.137503624, # Current PV power generation (W) - 'start_solution': start_solution, # Initial solution for the optimization - 'haushaltsgeraet_wh': 937, # Household appliance consumption (Wh) - 'haushaltsgeraet_dauer': 0 # Duration of appliance usage (hours) + # Cost of storing energy in battery (per Wh) + "preis_euro_pro_wh_akku": 10e-05, + # Initial state of charge (SOC) of PV battery (%) + "pv_soc": 80, + # Battery capacity (in Wh) + "pv_akku_cap": 26400, + # Yearly energy consumption (in Wh) + "year_energy": 4100000, + # Feed-in tariff for exporting electricity (per Wh) + "einspeiseverguetung_euro_pro_wh": 7e-05, + # Maximum heating power (in W) + "max_heizleistung": 1000, + # Overall load on the system + "gesamtlast": gesamtlast, + # PV generation forecast (48 hours) + "pv_forecast": pv_forecast, + # Temperature forecast (48 hours) + "temperature_forecast": temperature_forecast, + # Electricity price forecast (48 hours) + "strompreis_euro_pro_wh": strompreis_euro_pro_wh, + # Minimum SOC for electric car + "eauto_min_soc": 0, + # Electric car battery capacity (Wh) + "eauto_cap": 60000, + # Charging efficiency of the electric car + "eauto_charge_efficiency": 0.95, + # Charging power of the electric car (W) + "eauto_charge_power": 11040, + # Current SOC of the electric car (%) + "eauto_soc": 54, + # Current PV power generation (W) + "pvpowernow": 211.137503624, + # Initial solution for the optimization + "start_solution": start_solution, + # Household appliance consumption (Wh) + "haushaltsgeraet_wh": 937, + # Duration of appliance usage (hours) + "haushaltsgeraet_dauer": 0, } # Initialize the optimization problem diff --git a/tests/conftest.py b/tests/conftest.py index 91c9a65..e69de29 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1 +0,0 @@ -from test_heatpump import heatpump \ No newline at end of file diff --git a/tests/test_class_akku.py b/tests/test_class_akku.py index 0f88855..bb1a03c 100644 --- a/tests/test_class_akku.py +++ b/tests/test_class_akku.py @@ -1,9 +1,9 @@ import unittest -import numpy as np + from modules.class_akku import PVAkku -class TestPVAkku(unittest.TestCase): +class TestPVAkku(unittest.TestCase): def setUp(self): # Initializing common parameters for tests self.kapazitaet_wh = 10000 # 10,000 Wh capacity @@ -13,54 +13,129 @@ def setUp(self): self.max_soc_prozent = 80 # Maximum SoC is 80% def test_initial_state_of_charge(self): - akku = PVAkku(self.kapazitaet_wh, hours=1, start_soc_prozent=50, min_soc_prozent=self.min_soc_prozent, max_soc_prozent=self.max_soc_prozent) - self.assertEqual(akku.ladezustand_in_prozent(), 50.0, "Initial SoC should be 50%") + akku = PVAkku( + self.kapazitaet_wh, + hours=1, + start_soc_prozent=50, + min_soc_prozent=self.min_soc_prozent, + max_soc_prozent=self.max_soc_prozent, + ) + self.assertEqual( + akku.ladezustand_in_prozent(), 50.0, "Initial SoC should be 50%" + ) def test_discharge_below_min_soc(self): - akku = PVAkku(self.kapazitaet_wh, hours=1, start_soc_prozent=50, min_soc_prozent=self.min_soc_prozent, max_soc_prozent=self.max_soc_prozent) + akku = PVAkku( + self.kapazitaet_wh, + hours=1, + start_soc_prozent=50, + min_soc_prozent=self.min_soc_prozent, + max_soc_prozent=self.max_soc_prozent, + ) akku.reset() # Try to discharge more energy than available above min_soc - abgegeben_wh, verlust_wh = akku.energie_abgeben(5000, 0) # Try to discharge 5000 Wh + abgegeben_wh, verlust_wh = akku.energie_abgeben( + 5000, 0 + ) # Try to discharge 5000 Wh expected_soc = self.min_soc_prozent # SoC should not drop below min_soc - self.assertEqual(akku.ladezustand_in_prozent(), expected_soc, "SoC should not drop below min_soc after discharge") - self.assertEqual(abgegeben_wh, 2640.0, "The energy discharged should be limited by min_soc") + self.assertEqual( + akku.ladezustand_in_prozent(), + expected_soc, + "SoC should not drop below min_soc after discharge", + ) + self.assertEqual( + abgegeben_wh, 2640.0, "The energy discharged should be limited by min_soc" + ) def test_charge_above_max_soc(self): - akku = PVAkku(self.kapazitaet_wh, hours=1, start_soc_prozent=50, min_soc_prozent=self.min_soc_prozent, max_soc_prozent=self.max_soc_prozent) + akku = PVAkku( + self.kapazitaet_wh, + hours=1, + start_soc_prozent=50, + min_soc_prozent=self.min_soc_prozent, + max_soc_prozent=self.max_soc_prozent, + ) akku.reset() # Try to charge more energy than available up to max_soc geladen_wh, verlust_wh = akku.energie_laden(5000, 0) # Try to charge 5000 Wh expected_soc = self.max_soc_prozent # SoC should not exceed max_soc - self.assertEqual(akku.ladezustand_in_prozent(), expected_soc, "SoC should not exceed max_soc after charge") - self.assertEqual(geladen_wh, 3000.0, "The energy charged should be limited by max_soc") + self.assertEqual( + akku.ladezustand_in_prozent(), + expected_soc, + "SoC should not exceed max_soc after charge", + ) + self.assertEqual( + geladen_wh, 3000.0, "The energy charged should be limited by max_soc" + ) def test_charging_at_max_soc(self): - akku = PVAkku(self.kapazitaet_wh, hours=1, start_soc_prozent=80, min_soc_prozent=self.min_soc_prozent, max_soc_prozent=self.max_soc_prozent) + akku = PVAkku( + self.kapazitaet_wh, + hours=1, + start_soc_prozent=80, + min_soc_prozent=self.min_soc_prozent, + max_soc_prozent=self.max_soc_prozent, + ) akku.reset() # Try to charge when SoC is already at max_soc geladen_wh, verlust_wh = akku.energie_laden(5000, 0) self.assertEqual(geladen_wh, 0.0, "No energy should be charged when at max_soc") - self.assertEqual(akku.ladezustand_in_prozent(), self.max_soc_prozent, "SoC should remain at max_soc") + self.assertEqual( + akku.ladezustand_in_prozent(), + self.max_soc_prozent, + "SoC should remain at max_soc", + ) def test_discharging_at_min_soc(self): - akku = PVAkku(self.kapazitaet_wh, hours=1, start_soc_prozent=20, min_soc_prozent=self.min_soc_prozent, max_soc_prozent=self.max_soc_prozent) + akku = PVAkku( + self.kapazitaet_wh, + hours=1, + start_soc_prozent=20, + min_soc_prozent=self.min_soc_prozent, + max_soc_prozent=self.max_soc_prozent, + ) akku.reset() # Try to discharge when SoC is already at min_soc abgegeben_wh, verlust_wh = akku.energie_abgeben(5000, 0) - self.assertEqual(abgegeben_wh, 0.0, "No energy should be discharged when at min_soc") - self.assertEqual(akku.ladezustand_in_prozent(), self.min_soc_prozent, "SoC should remain at min_soc") + self.assertEqual( + abgegeben_wh, 0.0, "No energy should be discharged when at min_soc" + ) + self.assertEqual( + akku.ladezustand_in_prozent(), + self.min_soc_prozent, + "SoC should remain at min_soc", + ) def test_soc_limits(self): # Test to ensure that SoC never exceeds max_soc or drops below min_soc - akku = PVAkku(self.kapazitaet_wh, hours=1, start_soc_prozent=50, min_soc_prozent=self.min_soc_prozent, max_soc_prozent=self.max_soc_prozent) + akku = PVAkku( + self.kapazitaet_wh, + hours=1, + start_soc_prozent=50, + min_soc_prozent=self.min_soc_prozent, + max_soc_prozent=self.max_soc_prozent, + ) akku.reset() - akku.soc_wh = (self.max_soc_prozent / 100) * self.kapazitaet_wh + 1000 # Manually set SoC above max limit + akku.soc_wh = ( + self.max_soc_prozent / 100 + ) * self.kapazitaet_wh + 1000 # Manually set SoC above max limit akku.soc_wh = min(akku.soc_wh, akku.max_soc_wh) - self.assertLessEqual(akku.ladezustand_in_prozent(), self.max_soc_prozent, "SoC should not exceed max_soc") - - akku.soc_wh = (self.min_soc_prozent / 100) * self.kapazitaet_wh - 1000 # Manually set SoC below min limit + self.assertLessEqual( + akku.ladezustand_in_prozent(), + self.max_soc_prozent, + "SoC should not exceed max_soc", + ) + + akku.soc_wh = ( + self.min_soc_prozent / 100 + ) * self.kapazitaet_wh - 1000 # Manually set SoC below min limit akku.soc_wh = max(akku.soc_wh, akku.min_soc_wh) - self.assertGreaterEqual(akku.ladezustand_in_prozent(), self.min_soc_prozent, "SoC should not drop below min_soc") + self.assertGreaterEqual( + akku.ladezustand_in_prozent(), + self.min_soc_prozent, + "SoC should not drop below min_soc", + ) + -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_heatpump.py b/tests/test_heatpump.py index 81c0f34..6d3c7a7 100644 --- a/tests/test_heatpump.py +++ b/tests/test_heatpump.py @@ -1,13 +1,14 @@ import pytest + from modules.class_heatpump import Heatpump -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def heatpump() -> Heatpump: - """ Heatpump with 5 kw heating power and 24 h prediction - """ + """Heatpump with 5 kw heating power and 24 h prediction""" return Heatpump(5000, 24) + class TestHeatpump: def test_cop(self, heatpump): """Testing calculate COP for variouse outside temperatures"""