2024-11-15 22:27:25 +01:00
#!/usr/bin/env python3
2024-12-15 14:40:03 +01:00
import subprocess
import sys
from contextlib import asynccontextmanager
2024-11-15 22:27:25 +01:00
from pathlib import Path
2024-12-15 14:40:03 +01:00
from typing import Annotated , Any , AsyncGenerator , Dict , List , Optional , Union
2024-11-15 22:27:25 +01:00
2024-12-15 14:40:03 +01:00
import httpx
import pandas as pd
2024-11-15 22:27:25 +01:00
import uvicorn
2024-12-15 14:40:03 +01:00
from fastapi import FastAPI , Query , Request
2024-11-15 22:27:25 +01:00
from fastapi . exceptions import HTTPException
2024-12-15 14:40:03 +01:00
from fastapi . responses import FileResponse , RedirectResponse , Response
from pendulum import DateTime
2024-11-15 22:27:25 +01:00
2024-12-15 14:40:03 +01:00
from akkudoktoreos . config . config import ConfigEOS , SettingsEOS , get_config
from akkudoktoreos . core . pydantic import PydanticBaseModel
2024-11-19 21:47:43 +01:00
from akkudoktoreos . optimization . genetic import (
OptimizationParameters ,
OptimizeResponse ,
optimization_problem ,
)
2024-12-15 14:40:03 +01:00
# Still to be adapted
2024-11-19 21:47:43 +01:00
from akkudoktoreos . prediction . load_container import Gesamtlast
from akkudoktoreos . prediction . load_corrector import LoadPredictionAdjuster
from akkudoktoreos . prediction . load_forecast import LoadForecast
2024-12-15 14:40:03 +01:00
from akkudoktoreos . prediction . prediction import get_prediction
from akkudoktoreos . utils . logutil import get_logger
logger = get_logger ( __name__ )
config_eos = get_config ( )
prediction_eos = get_prediction ( )
@asynccontextmanager
async def lifespan ( app : FastAPI ) - > AsyncGenerator [ None , None ] :
""" Lifespan manager for the app. """
# On startup
if config_eos . server_fasthtml_host and config_eos . server_fasthtml_port :
try :
fasthtml_process = start_fasthtml_server ( )
except Exception as e :
logger . error ( f " Failed to start FastHTML server. Error: { e } " )
sys . exit ( 1 )
# Handover to application
yield
# On shutdown
# nothing to do
2024-11-15 22:27:25 +01:00
app = FastAPI (
title = " Akkudoktor-EOS " ,
description = " 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. " ,
summary = " Comprehensive solution for simulating and optimizing an energy system based on renewable energy sources " ,
version = " 0.0.1 " ,
license_info = {
" name " : " Apache 2.0 " ,
" url " : " https://www.apache.org/licenses/LICENSE-2.0.html " ,
} ,
2024-12-15 14:40:03 +01:00
lifespan = lifespan ,
2024-11-15 22:27:25 +01:00
)
2024-12-15 14:40:03 +01:00
# That's the problem
opt_class = optimization_problem ( )
2024-11-15 22:27:25 +01:00
server_dir = Path ( __file__ ) . parent . resolve ( )
class PdfResponse ( FileResponse ) :
media_type = " application/pdf "
2024-12-15 14:40:03 +01:00
@app.get ( " /config " )
def fastapi_config_get ( ) - > ConfigEOS :
""" Get the current configuration. """
return config_eos
@app.put ( " /config " )
def fastapi_config_put ( settings : SettingsEOS ) - > ConfigEOS :
""" Merge settings into current configuration. """
config_eos . merge_settings ( settings )
return config_eos
@app.get ( " /prediction/keys " )
def fastapi_prediction_keys ( ) - > list [ str ] :
""" Get a list of available prediction keys. """
return sorted ( list ( prediction_eos . keys ( ) ) )
@app.get ( " /prediction " )
def fastapi_prediction ( key : str ) - > list [ Union [ float | str ] ] :
""" Get the current configuration. """
values = prediction_eos [ key ] . to_list ( )
return values
2024-11-15 22:27:25 +01:00
@app.get ( " /strompreis " )
def fastapi_strompreis ( ) - > list [ float ] :
# Get the current date and the end date based on prediction hours
2024-12-15 14:40:03 +01:00
marketprice_series = prediction_eos [ " elecprice_marketprice " ]
# Fetch prices for the specified date range
specific_date_prices = marketprice_series . loc [
prediction_eos . start_datetime : prediction_eos . end_datetime
]
2024-11-15 22:27:25 +01:00
return specific_date_prices . tolist ( )
2024-12-15 14:40:03 +01:00
class GesamtlastRequest ( PydanticBaseModel ) :
2024-12-11 10:23:08 +01:00
year_energy : float
measured_data : List [ Dict [ str , Any ] ]
hours : int
2024-11-15 22:27:25 +01:00
@app.post ( " /gesamtlast " )
2024-12-11 10:23:08 +01:00
def fastapi_gesamtlast ( request : GesamtlastRequest ) - > list [ float ] :
2024-11-15 22:27:25 +01:00
""" Endpoint to handle total load calculation based on the latest measured data. """
2024-12-11 10:23:08 +01:00
# Request-Daten extrahieren
year_energy = request . year_energy
measured_data = request . measured_data
hours = request . hours
# Ab hier bleibt der Code unverändert ...
2024-11-15 22:27:25 +01:00
measured_data_df = pd . DataFrame ( measured_data )
measured_data_df [ " time " ] = pd . to_datetime ( measured_data_df [ " time " ] )
2024-12-11 10:23:08 +01:00
# Zeitzonenmanagement
2024-11-15 22:27:25 +01:00
if measured_data_df [ " time " ] . dt . tz is None :
measured_data_df [ " time " ] = measured_data_df [ " time " ] . dt . tz_localize ( " Europe/Berlin " )
else :
measured_data_df [ " time " ] = measured_data_df [ " time " ] . dt . tz_convert ( " Europe/Berlin " )
2024-12-11 10:23:08 +01:00
# Zeitzone entfernen
2024-11-15 22:27:25 +01:00
measured_data_df [ " time " ] = measured_data_df [ " time " ] . dt . tz_localize ( None )
2024-12-11 10:23:08 +01:00
# Forecast erstellen
2024-11-15 22:27:25 +01:00
lf = LoadForecast (
filepath = server_dir / " .. " / " data " / " load_profiles.npz " , year_energy = year_energy
)
forecast_list = [ ]
for single_date in pd . date_range (
measured_data_df [ " time " ] . min ( ) . date ( ) , measured_data_df [ " time " ] . max ( ) . date ( )
) :
date_str = single_date . strftime ( " % Y- % m- %d " )
daily_forecast = lf . get_daily_stats ( date_str )
mean_values = daily_forecast [ 0 ]
fc_hours = [ single_date + pd . Timedelta ( hours = i ) for i in range ( 24 ) ]
daily_forecast_df = pd . DataFrame ( { " time " : fc_hours , " Last Pred " : mean_values } )
forecast_list . append ( daily_forecast_df )
predicted_data = pd . concat ( forecast_list , ignore_index = True )
adjuster = LoadPredictionAdjuster ( measured_data_df , predicted_data , lf )
2024-12-11 10:23:08 +01:00
adjuster . calculate_weighted_mean ( )
adjuster . adjust_predictions ( )
future_predictions = adjuster . predict_next_hours ( hours )
2024-11-15 22:27:25 +01:00
2024-12-11 10:26:20 +01:00
leistung_haushalt = future_predictions [ " Adjusted Pred " ] . to_numpy ( )
2024-11-15 22:27:25 +01:00
gesamtlast = Gesamtlast ( prediction_hours = hours )
gesamtlast . hinzufuegen (
2024-11-26 22:28:05 +01:00
" Haushalt " ,
2024-12-11 10:23:08 +01:00
leistung_haushalt ,
)
2024-11-15 22:27:25 +01:00
2024-12-11 10:23:08 +01:00
last = gesamtlast . gesamtlast_berechnen ( )
2024-11-15 22:27:25 +01:00
return last . tolist ( )
@app.get ( " /gesamtlast_simple " )
def fastapi_gesamtlast_simple ( year_energy : float ) - > list [ float ] :
###############
# Load Forecast
###############
lf = LoadForecast (
filepath = server_dir / " .. " / " data " / " load_profiles.npz " , year_energy = year_energy
) # Instantiate LoadForecast with specified parameters
2024-12-15 14:40:03 +01:00
leistung_haushalt = lf . get_stats_for_date_range (
prediction_eos . start_datetime , prediction_eos . end_datetime
) [ 0 ] # Get expected household load for the date range
2024-11-15 22:27:25 +01:00
2024-12-15 14:40:03 +01:00
prediction_hours = config_eos . prediction_hours if config_eos . prediction_hours else 48
gesamtlast = Gesamtlast ( prediction_hours = prediction_hours ) # Create Gesamtlast instance
gesamtlast . hinzufuegen ( " Haushalt " , leistung_haushalt ) # Add household to total load calculation
2024-11-15 22:27:25 +01:00
# ###############
# # WP (Heat Pump)
# ##############
# leistung_wp = wp.simulate_24h(temperature_forecast) # Simulate heat pump load for 24 hours
# gesamtlast.hinzufuegen("Heatpump", leistung_wp) # Add heat pump load to total load calculation
last = gesamtlast . gesamtlast_berechnen ( ) # Calculate total load
return last . tolist ( ) # Return total load as JSON
2024-12-15 14:40:03 +01:00
class ForecastResponse ( PydanticBaseModel ) :
temperature : list [ float ]
pvpower : list [ float ]
2024-11-15 22:27:25 +01:00
2024-12-15 14:40:03 +01:00
@app.get ( " /pvforecast " )
def fastapi_pvprognose ( ac_power_measurement : Optional [ float ] = None ) - > ForecastResponse :
2024-11-15 22:27:25 +01:00
###############
# PV Forecast
###############
2024-12-15 14:40:03 +01:00
pvforecast_ac_power = prediction_eos [ " pvforecast_ac_power " ]
# Fetch prices for the specified date range
pvforecast_ac_power = pvforecast_ac_power . loc [
prediction_eos . start_datetime : prediction_eos . end_datetime
]
pvforecastakkudoktor_temp_air = prediction_eos [ " pvforecastakkudoktor_temp_air " ]
# Fetch prices for the specified date range
pvforecastakkudoktor_temp_air = pvforecastakkudoktor_temp_air . loc [
prediction_eos . start_datetime : prediction_eos . end_datetime
]
# Return both forecasts as a JSON response
return ForecastResponse (
temperature = pvforecastakkudoktor_temp_air . tolist ( ) , pvpower = pvforecast_ac_power . tolist ( )
)
2024-11-15 22:27:25 +01:00
@app.post ( " /optimize " )
def fastapi_optimize (
parameters : OptimizationParameters ,
start_hour : Annotated [
Optional [ int ] , Query ( description = " Defaults to current hour of the day. " )
] = None ,
) - > OptimizeResponse :
if start_hour is None :
2024-12-15 14:40:03 +01:00
start_hour = DateTime . now ( ) . hour
# TODO: Remove when config and prediction update is done by EMS.
config_eos . update ( )
prediction_eos . update_data ( )
2024-11-15 22:27:25 +01:00
# Perform optimization simulation
result = opt_class . optimierung_ems ( parameters = parameters , start_hour = start_hour )
# print(result)
return result
@app.get ( " /visualization_results.pdf " , response_class = PdfResponse )
2024-11-26 22:28:05 +01:00
def get_pdf ( ) - > PdfResponse :
2024-11-15 22:27:25 +01:00
# Endpoint to serve the generated PDF with visualization results
2024-12-15 14:40:03 +01:00
output_path = config_eos . data_output_path
2024-11-15 22:27:25 +01:00
if not output_path . is_dir ( ) :
2024-12-15 14:40:03 +01:00
raise ValueError ( f " Output path does not exist: { output_path } . " )
2024-11-15 22:27:25 +01:00
file_path = output_path / " visualization_results.pdf "
if not file_path . is_file ( ) :
raise HTTPException ( status_code = 404 , detail = " No visualization result available. " )
2024-11-26 22:28:05 +01:00
return PdfResponse ( file_path )
2024-11-15 22:27:25 +01:00
@app.get ( " /site-map " , include_in_schema = False )
2024-11-26 22:28:05 +01:00
def site_map ( ) - > RedirectResponse :
2024-11-15 22:27:25 +01:00
return RedirectResponse ( url = " /docs " )
2024-12-15 14:40:03 +01:00
# Keep the proxy last to handle all requests that are not taken by the Rest API.
# Also keep the single endpoints for delete, get, post, put to assure openapi.json is always build
# the same way for testing.
2024-11-15 22:27:25 +01:00
2024-12-15 14:40:03 +01:00
@app.delete ( " / { path:path} " )
async def proxy_delete ( request : Request , path : str ) - > Response :
return await proxy ( request , path )
@app.get ( " / { path:path} " )
async def proxy_get ( request : Request , path : str ) - > Response :
return await proxy ( request , path )
@app.post ( " / { path:path} " )
async def proxy_post ( request : Request , path : str ) - > Response :
return await proxy ( request , path )
@app.put ( " / { path:path} " )
async def proxy_put ( request : Request , path : str ) - > Response :
return await proxy ( request , path )
2024-11-15 22:27:25 +01:00
2024-12-15 14:40:03 +01:00
async def proxy ( request : Request , path : str ) - > Union [ Response | RedirectResponse ] :
if config_eos . server_fasthtml_host and config_eos . server_fasthtml_port :
# Proxy to fasthtml server
url = f " http:// { config_eos . server_fasthtml_host } : { config_eos . server_fasthtml_port } / { path } "
headers = dict ( request . headers )
data = await request . body ( )
async with httpx . AsyncClient ( ) as client :
if request . method == " GET " :
response = await client . get ( url , headers = headers )
elif request . method == " POST " :
response = await client . post ( url , headers = headers , content = data )
elif request . method == " PUT " :
response = await client . put ( url , headers = headers , content = data )
elif request . method == " DELETE " :
response = await client . delete ( url , headers = headers , content = data )
return Response (
content = response . content ,
status_code = response . status_code ,
headers = dict ( response . headers ) ,
)
else :
# Redirect the root URL to the site map
return RedirectResponse ( url = " /docs " )
def start_fasthtml_server ( ) - > subprocess . Popen :
""" Start the fasthtml server as a subprocess. """
server_process = subprocess . Popen (
[ sys . executable , str ( server_dir . joinpath ( " fasthtml_server.py " ) ) ] ,
stdout = subprocess . PIPE ,
stderr = subprocess . PIPE ,
)
return server_process
def start_fastapi_server ( ) - > None :
""" Start FastAPI server. """
2024-11-15 22:27:25 +01:00
try :
2024-12-15 14:40:03 +01:00
uvicorn . run (
app ,
host = str ( config_eos . server_fastapi_host ) ,
port = config_eos . server_fastapi_port ,
log_level = " debug " ,
access_log = True ,
)
2024-11-15 22:27:25 +01:00
except Exception as e :
2024-12-15 14:40:03 +01:00
logger . error (
f " Could not bind to host { config_eos . server_fastapi_host } : { config_eos . server_fastapi_port } . Error: { e } "
)
sys . exit ( 1 )
if __name__ == " __main__ " :
start_fastapi_server ( )