diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..39a608f --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,13 @@ +version: 2 + +build: + os: ubuntu-24.04 + tools: + python: "3.12" + +sphinx: + configuration: docs/conf.py + +python: + install: + - requirements: requirements-dev.txt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d78e730..71caee0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,10 @@ Thanks for taking the time to read this! The `EOS` project is in early development, therefore we encourage contribution in the following ways: +## Documentation + +Latest development documentation can be found at [Akkudoktor-EOS](https://akkudoktor-eos.readthedocs.io/en/main/). + ## Bug Reports Please report flaws or vulnerabilities in the [GitHub Issue Tracker](https://github.com/Akkudoktor-EOS/EOS/issues) using the corresponding issue template. diff --git a/Makefile b/Makefile index 08cc203..a5c4e80 100644 --- a/Makefile +++ b/Makefile @@ -54,6 +54,7 @@ dist: pip # Target to generate documentation gen-docs: pip-dev + .venv/bin/pip install -e . .venv/bin/python ./scripts/generate_config_md.py --output-file docs/_generated/config.md .venv/bin/python ./scripts/generate_openapi_md.py --output-file docs/_generated/openapi.md .venv/bin/python ./scripts/generate_openapi.py --output-file openapi.json diff --git a/README.md b/README.md index 37eb0fb..69eff72 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ This project provides a comprehensive solution for simulating and optimizing an energy system based on renewable energy sources. With a focus on photovoltaic (PV) systems, battery storage (batteries), load management (consumer requirements), heat pumps, electric vehicles, and consideration of electricity price data, this system enables forecasting and optimization of energy flow and costs over a specified period. +Documentation can be found at [Akkudoktor-EOS](https://akkudoktor-eos.readthedocs.io/en/latest/). + ## Getting Involved See [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/src/akkudoktoreos/class_soc_calc.py b/src/akkudoktoreos/class_soc_calc.py deleted file mode 100644 index 4b75f55..0000000 --- a/src/akkudoktoreos/class_soc_calc.py +++ /dev/null @@ -1,340 +0,0 @@ -from datetime import datetime, timedelta - -import mariadb -import matplotlib.pyplot as plt -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, - ): - 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.conn = None - self.data = None - - def connect_db(self): - self.conn = mariadb.connect(**self.config) - self.cursor = self.conn.cursor() - - def disconnect_db(self): - if self.conn: - self.cursor.close() - self.conn.close() - - def fetch_data(self, start_time): - query = """ - SELECT timestamp, data, topic - FROM pip - WHERE timestamp >= %s AND (topic = 'battery_current' OR topic = 'battery_voltage') - ORDER BY timestamp - """ - 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) - - def process_data(self): - 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.columns.name = None - data_pivot.reset_index(inplace=True) - self.data = data_pivot - - def group_points(self, df): - 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 - ): - group.append(row) - else: - groups.append(group) - group = [row] - 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"] - ] - - last_points_100 = self.group_points(times_soc_100_all) - last_points_0 = self.group_points(times_soc_0_all) - - last_points_100_df = pd.DataFrame(last_points_100) - last_points_0_df = pd.DataFrame(last_points_0) - - return last_points_100_df, last_points_0_df - - 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") - - # Initialisieren der SoC-Liste - 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 - - if ( - not last_points_100_df.empty - and start_point["timestamp"] in last_points_100_df["timestamp"].values - ): - initial_soc = 100 - 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) - - 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 - - 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], - } - ) - print(integration_results) - 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 - if delta_soc > 0: # Avoid division by zero - 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_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"]) - ] - - self.cursor.executemany(delete_query, timestamps) - self.conn.commit() - - def update_database_with_soc(self, soc_df): - # Löschen der vorhandenen Einträge mit demselben Topic und Datum - 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')) - print(soc_df_resampled) - - # Einfügen der berechneten SoC-Werte in die Datenbank - insert_query = """ - INSERT INTO pip (timestamp, data, topic) - VALUES (%s, %s, 'calculated_soc') - """ - 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"], - ) - try: - self.cursor.execute(insert_query, record) - except mariadb.OperationalError as e: - print(f"Error inserting record {record}: {e}") - - 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", - ) - if not last_points_100_df.empty: - 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.subplot(4, 1, 2) - plt.plot( - self.data["timestamp"], - self.data["battery_current"], - label="Battery Current", - color="orange", - ) - if not last_points_100_df.empty: - 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.subplot(4, 1, 3) - 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.subplot(4, 1, 4) - # plt.plot(soh_df['timestamp'], soh_df['soh'], label='SoH', color='brown') - # plt.xlabel('Timestamp') - # plt.ylabel('SoH (%)') - # plt.legend() - # plt.title('State of Health (SoH) over Time') - - plt.tight_layout() - plt.show() - - -if __name__ == "__main__": - # MariaDB Verbindungsdetails - - config = { - "user": "soc", - "password": "Rayoflight123!", - "host": "192.168.1.135", - "database": "sensor", - } - - # Parameter festlegen - voltage_high_threshold = 55.4 # 100% SoC - voltage_low_threshold = 48 # 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 = 0.8 * 33 * 1000 / 48 - - # Zeitpunkt X definieren - zeitpunkt_x = (datetime.now() - timedelta(weeks=4)).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.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) - # 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/src/akkudoktoreos/core/__init__.py b/src/akkudoktoreos/core/__init__.py new file mode 100644 index 0000000..e69de29