mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2025-09-20 02:31:14 +00:00
Mypy (#217)
* Mypy: Initial support * Add to pre-commit (currently installs own deps, could maybe changed to poetry venv in the future to reuse environment and don't need duplicated types deps). * Add type hints. * Mypy: Add missing annotations
This commit is contained in:
committed by
GitHub
parent
2a163569bc
commit
1163ddb4ac
@@ -1,14 +1,15 @@
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Union
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
from pydantic import BaseModel, Field, field_validator, model_validator
|
||||
from typing_extensions import Self
|
||||
|
||||
from akkudoktoreos.config import EOSConfig
|
||||
from akkudoktoreos.devices.battery import PVAkku
|
||||
from akkudoktoreos.devices.generic import HomeAppliance
|
||||
from akkudoktoreos.devices.inverter import Wechselrichter
|
||||
from akkudoktoreos.utils.utils import NumpyEncoder
|
||||
|
||||
|
||||
class EnergieManagementSystemParameters(BaseModel):
|
||||
@@ -41,14 +42,67 @@ class EnergieManagementSystemParameters(BaseModel):
|
||||
return self
|
||||
|
||||
|
||||
class SimulationResult(BaseModel):
|
||||
"""This object contains the results of the simulation and provides insights into various parameters over the entire forecast period."""
|
||||
|
||||
Last_Wh_pro_Stunde: list[Optional[float]] = Field(description="TBD")
|
||||
EAuto_SoC_pro_Stunde: list[Optional[float]] = Field(
|
||||
description="The state of charge of the EV for each hour."
|
||||
)
|
||||
Einnahmen_Euro_pro_Stunde: list[Optional[float]] = Field(
|
||||
description="The revenue from grid feed-in or other sources in euros per hour."
|
||||
)
|
||||
Gesamt_Verluste: float = Field(
|
||||
description="The total losses in watt-hours over the entire period."
|
||||
)
|
||||
Gesamtbilanz_Euro: float = Field(
|
||||
description="The total balance of revenues minus costs in euros."
|
||||
)
|
||||
Gesamteinnahmen_Euro: float = Field(description="The total revenues in euros.")
|
||||
Gesamtkosten_Euro: float = Field(description="The total costs in euros.")
|
||||
Home_appliance_wh_per_hour: list[Optional[float]] = Field(
|
||||
description="The energy consumption of a household appliance in watt-hours per hour."
|
||||
)
|
||||
Kosten_Euro_pro_Stunde: list[Optional[float]] = Field(
|
||||
description="The costs in euros per hour."
|
||||
)
|
||||
Netzbezug_Wh_pro_Stunde: list[Optional[float]] = Field(
|
||||
description="The grid energy drawn in watt-hours per hour."
|
||||
)
|
||||
Netzeinspeisung_Wh_pro_Stunde: list[Optional[float]] = Field(
|
||||
description="The energy fed into the grid in watt-hours per hour."
|
||||
)
|
||||
Verluste_Pro_Stunde: list[Optional[float]] = Field(
|
||||
description="The losses in watt-hours per hour."
|
||||
)
|
||||
akku_soc_pro_stunde: list[Optional[float]] = Field(
|
||||
description="The state of charge of the battery (not the EV) in percentage per hour."
|
||||
)
|
||||
|
||||
@field_validator(
|
||||
"Last_Wh_pro_Stunde",
|
||||
"Netzeinspeisung_Wh_pro_Stunde",
|
||||
"akku_soc_pro_stunde",
|
||||
"Netzbezug_Wh_pro_Stunde",
|
||||
"Kosten_Euro_pro_Stunde",
|
||||
"Einnahmen_Euro_pro_Stunde",
|
||||
"EAuto_SoC_pro_Stunde",
|
||||
"Verluste_Pro_Stunde",
|
||||
"Home_appliance_wh_per_hour",
|
||||
mode="before",
|
||||
)
|
||||
def convert_numpy(cls, field: Any) -> Any:
|
||||
return NumpyEncoder.convert_numpy(field)[0]
|
||||
|
||||
|
||||
class EnergieManagementSystem:
|
||||
def __init__(
|
||||
self,
|
||||
config: EOSConfig,
|
||||
parameters: EnergieManagementSystemParameters,
|
||||
wechselrichter: Wechselrichter,
|
||||
eauto: Optional[PVAkku] = None,
|
||||
home_appliance: Optional[HomeAppliance] = None,
|
||||
wechselrichter: Optional[Wechselrichter] = None,
|
||||
):
|
||||
self.akku = wechselrichter.akku
|
||||
self.gesamtlast = np.array(parameters.gesamtlast, float)
|
||||
@@ -66,7 +120,7 @@ class EnergieManagementSystem:
|
||||
self.dc_charge_hours = np.full(config.prediction_hours, 1)
|
||||
self.ev_charge_hours = np.full(config.prediction_hours, 0)
|
||||
|
||||
def set_akku_discharge_hours(self, ds: List[int]) -> None:
|
||||
def set_akku_discharge_hours(self, ds: np.ndarray) -> None:
|
||||
self.akku.set_discharge_per_hour(ds)
|
||||
|
||||
def set_akku_ac_charge_hours(self, ds: np.ndarray) -> None:
|
||||
@@ -75,22 +129,24 @@ class EnergieManagementSystem:
|
||||
def set_akku_dc_charge_hours(self, ds: np.ndarray) -> None:
|
||||
self.dc_charge_hours = ds
|
||||
|
||||
def set_ev_charge_hours(self, ds: List[int]) -> None:
|
||||
def set_ev_charge_hours(self, ds: np.ndarray) -> None:
|
||||
self.ev_charge_hours = ds
|
||||
|
||||
def set_home_appliance_start(self, ds: List[int], global_start_hour: int = 0) -> None:
|
||||
self.home_appliance.set_starting_time(ds, global_start_hour=global_start_hour)
|
||||
def set_home_appliance_start(self, start_hour: int, global_start_hour: int = 0) -> None:
|
||||
assert self.home_appliance is not None
|
||||
self.home_appliance.set_starting_time(start_hour, global_start_hour=global_start_hour)
|
||||
|
||||
def reset(self) -> None:
|
||||
self.eauto.reset()
|
||||
if self.eauto:
|
||||
self.eauto.reset()
|
||||
self.akku.reset()
|
||||
|
||||
def simuliere_ab_jetzt(self) -> dict:
|
||||
def simuliere_ab_jetzt(self) -> dict[str, Any]:
|
||||
jetzt = datetime.now()
|
||||
start_stunde = jetzt.hour
|
||||
return self.simuliere(start_stunde)
|
||||
|
||||
def simuliere(self, start_stunde: int) -> dict:
|
||||
def simuliere(self, start_stunde: int) -> dict[str, Any]:
|
||||
"""hour.
|
||||
|
||||
akku_soc_pro_stunde begin of the hour, initial hour state!
|
||||
|
@@ -2,11 +2,13 @@ import numpy as np
|
||||
|
||||
|
||||
class Gesamtlast:
|
||||
def __init__(self, prediction_hours=24):
|
||||
self.lasten = {} # Contains names and load arrays for different sources
|
||||
def __init__(self, prediction_hours: int = 24):
|
||||
self.lasten: dict[
|
||||
str, np.ndarray
|
||||
] = {} # Contains names and load arrays for different sources
|
||||
self.prediction_hours = prediction_hours
|
||||
|
||||
def hinzufuegen(self, name, last_array):
|
||||
def hinzufuegen(self, name: str, last_array: np.ndarray) -> None:
|
||||
"""Adds an array of loads for a specific source.
|
||||
|
||||
:param name: Name of the load source (e.g., "Household", "Heat Pump")
|
||||
@@ -16,13 +18,13 @@ class Gesamtlast:
|
||||
raise ValueError(f"Total load inconsistent lengths in arrays: {name} {len(last_array)}")
|
||||
self.lasten[name] = last_array
|
||||
|
||||
def gesamtlast_berechnen(self):
|
||||
def gesamtlast_berechnen(self) -> np.ndarray:
|
||||
"""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 []
|
||||
return np.ndarray(0)
|
||||
|
||||
# Assumption: All load arrays have the same length
|
||||
stunden = len(next(iter(self.lasten.values())))
|
||||
|
@@ -1,28 +1,30 @@
|
||||
from datetime import datetime
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.metrics import mean_squared_error, r2_score
|
||||
|
||||
from akkudoktoreos.prediction.load_forecast import LoadForecast
|
||||
|
||||
|
||||
class LoadPredictionAdjuster:
|
||||
def __init__(self, measured_data, predicted_data, load_forecast):
|
||||
def __init__(
|
||||
self, measured_data: pd.DataFrame, predicted_data: pd.DataFrame, load_forecast: LoadForecast
|
||||
):
|
||||
self.measured_data = measured_data
|
||||
self.predicted_data = predicted_data
|
||||
self.load_forecast = load_forecast
|
||||
self.merged_data = self._merge_data()
|
||||
self.train_data = None
|
||||
self.test_data = None
|
||||
self.weekday_diff = None
|
||||
self.weekend_diff = None
|
||||
|
||||
def _remove_outliers(self, data, threshold=2):
|
||||
def _remove_outliers(self, data: pd.DataFrame, threshold: int = 2) -> pd.DataFrame:
|
||||
# Calculate the Z-Score of the 'Last' data
|
||||
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"])
|
||||
|
||||
def _merge_data(self):
|
||||
def _merge_data(self) -> pd.DataFrame:
|
||||
# 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"])
|
||||
@@ -47,7 +49,9 @@ class LoadPredictionAdjuster:
|
||||
merged_data["DayOfWeek"] = merged_data["time"].dt.dayofweek
|
||||
return merged_data
|
||||
|
||||
def calculate_weighted_mean(self, train_period_weeks=9, test_period_weeks=1):
|
||||
def calculate_weighted_mean(
|
||||
self, train_period_weeks: int = 9, test_period_weeks: int = 1
|
||||
) -> None:
|
||||
self.merged_data = self._remove_outliers(self.merged_data)
|
||||
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)
|
||||
@@ -79,27 +83,27 @@ class LoadPredictionAdjuster:
|
||||
weekends_train_data.groupby("Hour").apply(self._weighted_mean_diff).dropna()
|
||||
)
|
||||
|
||||
def _weighted_mean_diff(self, data):
|
||||
def _weighted_mean_diff(self, data: pd.DataFrame) -> float:
|
||||
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):
|
||||
def adjust_predictions(self) -> None:
|
||||
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):
|
||||
def _adjust_row(self, row: pd.Series) -> pd.Series:
|
||||
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)
|
||||
|
||||
def plot_results(self):
|
||||
def plot_results(self) -> None:
|
||||
self._plot_data(self.train_data, "Training")
|
||||
self._plot_data(self.test_data, "Testing")
|
||||
|
||||
def _plot_data(self, data, data_type):
|
||||
def _plot_data(self, data: pd.DataFrame, data_type: str) -> None:
|
||||
plt.figure(figsize=(14, 7))
|
||||
plt.plot(data["time"], data["Last"], label=f"Actual Last - {data_type}", color="blue")
|
||||
plt.plot(
|
||||
@@ -123,13 +127,13 @@ class LoadPredictionAdjuster:
|
||||
plt.grid(True)
|
||||
plt.show()
|
||||
|
||||
def evaluate_model(self):
|
||||
def evaluate_model(self) -> None:
|
||||
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):
|
||||
def predict_next_hours(self, hours_ahead: int) -> pd.DataFrame:
|
||||
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})
|
||||
@@ -139,7 +143,7 @@ class LoadPredictionAdjuster:
|
||||
future_df["Adjusted Pred"] = future_df.apply(self._adjust_row, axis=1)
|
||||
return future_df
|
||||
|
||||
def _forecast_next_hours(self, timestamp):
|
||||
def _forecast_next_hours(self, timestamp: datetime) -> float:
|
||||
date_str = timestamp.strftime("%Y-%m-%d")
|
||||
hour = timestamp.hour
|
||||
daily_forecast = self.load_forecast.get_daily_stats(date_str)
|
||||
|
@@ -1,4 +1,5 @@
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -6,14 +7,12 @@ import numpy as np
|
||||
|
||||
|
||||
class LoadForecast:
|
||||
def __init__(self, filepath=None, year_energy=None):
|
||||
def __init__(self, filepath: str | Path, year_energy: float):
|
||||
self.filepath = filepath
|
||||
self.data = None
|
||||
self.data_year_energy = None
|
||||
self.year_energy = year_energy
|
||||
self.load_data()
|
||||
|
||||
def get_daily_stats(self, date_str):
|
||||
def get_daily_stats(self, date_str: str) -> np.ndarray:
|
||||
"""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"
|
||||
@@ -29,7 +28,7 @@ class LoadForecast:
|
||||
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):
|
||||
def get_hourly_stats(self, date_str: str, hour: int) -> np.ndarray:
|
||||
"""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"
|
||||
@@ -47,7 +46,7 @@ class LoadForecast:
|
||||
|
||||
return hourly_stats
|
||||
|
||||
def get_stats_for_date_range(self, start_date_str, end_date_str):
|
||||
def get_stats_for_date_range(self, start_date_str: str, end_date_str: str) -> np.ndarray:
|
||||
"""Returns the means and standard deviations for a date range.
|
||||
|
||||
:param start_date_str: Start date as a string in the format "YYYY-MM-DD"
|
||||
@@ -69,7 +68,7 @@ class LoadForecast:
|
||||
stats_for_range = stats_for_range.reshape(stats_for_range.shape[0], -1)
|
||||
return stats_for_range
|
||||
|
||||
def load_data(self):
|
||||
def load_data(self) -> None:
|
||||
"""Loads data from the specified file."""
|
||||
try:
|
||||
data = np.load(self.filepath)
|
||||
@@ -81,11 +80,12 @@ class LoadForecast:
|
||||
except Exception as e:
|
||||
print(f"An error occurred while loading data: {e}")
|
||||
|
||||
def get_price_data(self):
|
||||
def get_price_data(self) -> None:
|
||||
"""Returns price data (currently not implemented)."""
|
||||
return self.price_data
|
||||
raise NotImplementedError
|
||||
# return self.price_data
|
||||
|
||||
def _convert_to_datetime(self, date_str):
|
||||
def _convert_to_datetime(self, date_str: str) -> datetime:
|
||||
"""Converts a date string to a datetime object."""
|
||||
return datetime.strptime(date_str, "%Y-%m-%d")
|
||||
|
||||
|
@@ -3,6 +3,7 @@ import json
|
||||
import zoneinfo
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Sequence
|
||||
|
||||
import numpy as np
|
||||
import requests
|
||||
@@ -10,7 +11,7 @@ import requests
|
||||
from akkudoktoreos.config import AppConfig, SetupIncomplete
|
||||
|
||||
|
||||
def repeat_to_shape(array, target_shape):
|
||||
def repeat_to_shape(array: np.ndarray, target_shape: Sequence[int]) -> np.ndarray:
|
||||
# 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")
|
||||
@@ -25,7 +26,11 @@ def repeat_to_shape(array, target_shape):
|
||||
|
||||
class HourlyElectricityPriceForecast:
|
||||
def __init__(
|
||||
self, source: str | Path, config: AppConfig, charges=0.000228, use_cache=True
|
||||
self,
|
||||
source: str | Path,
|
||||
config: AppConfig,
|
||||
charges: float = 0.000228,
|
||||
use_cache: bool = True,
|
||||
): # 228
|
||||
self.cache_dir = config.working_dir / config.directories.cache
|
||||
self.use_cache = use_cache
|
||||
@@ -37,7 +42,7 @@ class HourlyElectricityPriceForecast:
|
||||
self.charges = charges
|
||||
self.prediction_hours = config.eos.prediction_hours
|
||||
|
||||
def load_data(self, source: str | Path):
|
||||
def load_data(self, source: str | Path) -> list[dict[str, Any]]:
|
||||
cache_file = self.get_cache_file(source)
|
||||
if isinstance(source, str):
|
||||
if cache_file.is_file() and not self.is_cache_expired() and self.use_cache:
|
||||
@@ -61,12 +66,14 @@ class HourlyElectricityPriceForecast:
|
||||
raise ValueError(f"Input is not a valid path: {source}")
|
||||
return json_data["values"]
|
||||
|
||||
def get_cache_file(self, url):
|
||||
def get_cache_file(self, url: str | Path) -> Path:
|
||||
if isinstance(url, Path):
|
||||
url = str(url)
|
||||
hash_object = hashlib.sha256(url.encode())
|
||||
hex_dig = hash_object.hexdigest()
|
||||
return self.cache_dir / f"cache_{hex_dig}.json"
|
||||
|
||||
def is_cache_expired(self):
|
||||
def is_cache_expired(self) -> bool:
|
||||
if not self.cache_time_file.is_file():
|
||||
return True
|
||||
with self.cache_time_file.open("r") as file:
|
||||
@@ -74,11 +81,11 @@ class HourlyElectricityPriceForecast:
|
||||
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):
|
||||
def update_cache_timestamp(self) -> None:
|
||||
with self.cache_time_file.open("w") as file:
|
||||
file.write(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
||||
|
||||
def get_price_for_date(self, date_str):
|
||||
def get_price_for_date(self, date_str: str) -> np.ndarray:
|
||||
"""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")
|
||||
@@ -108,7 +115,7 @@ class HourlyElectricityPriceForecast:
|
||||
|
||||
return np.array(date_prices) / (1000.0 * 100.0) + self.charges
|
||||
|
||||
def get_price_for_daterange(self, start_date_str, end_date_str):
|
||||
def get_price_for_daterange(self, start_date_str: str, end_date_str: str) -> np.ndarray:
|
||||
"""Returns all prices between the start and end dates."""
|
||||
print(start_date_str)
|
||||
print(end_date_str)
|
||||
@@ -117,7 +124,7 @@ class HourlyElectricityPriceForecast:
|
||||
start_date = start_date_utc.astimezone(zoneinfo.ZoneInfo("Europe/Berlin"))
|
||||
end_date = end_date_utc.astimezone(zoneinfo.ZoneInfo("Europe/Berlin"))
|
||||
|
||||
price_list = []
|
||||
price_list: list[float] = []
|
||||
|
||||
while start_date < end_date:
|
||||
date_str = start_date.strftime("%Y-%m-%d")
|
||||
@@ -127,8 +134,10 @@ class HourlyElectricityPriceForecast:
|
||||
price_list.extend(daily_prices)
|
||||
start_date += timedelta(days=1)
|
||||
|
||||
price_list_np = np.array(price_list)
|
||||
|
||||
# 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,))
|
||||
price_list_np = repeat_to_shape(price_list_np, (self.prediction_hours,))
|
||||
|
||||
return price_list
|
||||
return price_list_np
|
||||
|
@@ -21,7 +21,7 @@ Example:
|
||||
)
|
||||
|
||||
# Update the AC power measurement for a specific date and time
|
||||
forecast.update_ac_power_measurement(date_time=datetime.now(), ac_power_measurement=1000)
|
||||
forecast.update_ac_power_measurement(ac_power_measurement=1000, date_time=datetime.now())
|
||||
|
||||
# Print the forecast data with DC and AC power details
|
||||
forecast.print_ac_power_and_measurement()
|
||||
@@ -36,7 +36,8 @@ Attributes:
|
||||
|
||||
import json
|
||||
from datetime import date, datetime
|
||||
from typing import List, Optional, Union
|
||||
from pathlib import Path
|
||||
from typing import Any, List, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
@@ -89,21 +90,20 @@ class AkkudoktorForecast(BaseModel):
|
||||
values: List[List[AkkudoktorForecastValue]]
|
||||
|
||||
|
||||
def validate_pv_forecast_data(data) -> str:
|
||||
def validate_pv_forecast_data(data: dict[str, Any]) -> Optional[str]:
|
||||
"""Validate PV forecast data."""
|
||||
data_type = None
|
||||
error_msg = ""
|
||||
|
||||
try:
|
||||
AkkudoktorForecast.model_validate(data)
|
||||
data_type = "Akkudoktor"
|
||||
except ValidationError as e:
|
||||
error_msg = ""
|
||||
for error in e.errors():
|
||||
field = " -> ".join(str(x) for x in error["loc"])
|
||||
message = error["msg"]
|
||||
error_type = error["type"]
|
||||
error_msg += f"Field: {field}\nError: {message}\nType: {error_type}\n"
|
||||
logger.debug(f"Validation did not succeed: {error_msg}")
|
||||
return None
|
||||
|
||||
return data_type
|
||||
|
||||
@@ -167,7 +167,7 @@ class ForecastData:
|
||||
"""
|
||||
return self.dc_power
|
||||
|
||||
def ac_power_measurement(self) -> float:
|
||||
def get_ac_power_measurement(self) -> Optional[float]:
|
||||
"""Returns the measured AC power.
|
||||
|
||||
It returns the measured AC power if available; otherwise None.
|
||||
@@ -191,7 +191,7 @@ class ForecastData:
|
||||
else:
|
||||
return self.ac_power
|
||||
|
||||
def get_windspeed_10m(self) -> float:
|
||||
def get_windspeed_10m(self) -> Optional[float]:
|
||||
"""Returns the wind speed at 10 meters altitude.
|
||||
|
||||
Returns:
|
||||
@@ -199,7 +199,7 @@ class ForecastData:
|
||||
"""
|
||||
return self.windspeed_10m
|
||||
|
||||
def get_temperature(self) -> float:
|
||||
def get_temperature(self) -> Optional[float]:
|
||||
"""Returns the temperature.
|
||||
|
||||
Returns:
|
||||
@@ -227,10 +227,10 @@ class PVForecast:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: Optional[dict] = None,
|
||||
filepath: Optional[str] = None,
|
||||
data: Optional[dict[str, Any]] = None,
|
||||
filepath: Optional[str | Path] = None,
|
||||
url: Optional[str] = None,
|
||||
forecast_start: Union[datetime, date, str, int, float] = None,
|
||||
forecast_start: Union[datetime, date, str, int, float, None] = None,
|
||||
prediction_hours: Optional[int] = None,
|
||||
):
|
||||
"""Initializes a `PVForecast` instance.
|
||||
@@ -253,16 +253,15 @@ class PVForecast:
|
||||
Example:
|
||||
forecast = PVForecast(data=my_forecast_data, forecast_start="2024-10-13", prediction_hours=72)
|
||||
"""
|
||||
self.meta = {}
|
||||
self.forecast_data = []
|
||||
self.current_measurement = None
|
||||
self.meta: dict[str, Any] = {}
|
||||
self.forecast_data: list[ForecastData] = []
|
||||
self.current_measurement: Optional[float] = None
|
||||
self.data = data
|
||||
self.filepath = filepath
|
||||
self.url = url
|
||||
self._forecast_start: Optional[datetime] = None
|
||||
if forecast_start:
|
||||
self._forecast_start = to_datetime(forecast_start, to_naiv=True, to_maxtime=False)
|
||||
else:
|
||||
self._forecast_start = None
|
||||
self.prediction_hours = prediction_hours
|
||||
self._tz_name = None
|
||||
|
||||
@@ -277,8 +276,8 @@ class PVForecast:
|
||||
|
||||
def update_ac_power_measurement(
|
||||
self,
|
||||
ac_power_measurement: float,
|
||||
date_time: Union[datetime, date, str, int, float, None] = None,
|
||||
ac_power_measurement=None,
|
||||
) -> bool:
|
||||
"""Updates the AC power measurement for a specific time.
|
||||
|
||||
@@ -309,10 +308,10 @@ class PVForecast:
|
||||
|
||||
def process_data(
|
||||
self,
|
||||
data: Optional[dict] = None,
|
||||
filepath: Optional[str] = None,
|
||||
data: Optional[dict[str, Any]] = None,
|
||||
filepath: Optional[str | Path] = None,
|
||||
url: Optional[str] = None,
|
||||
forecast_start: Union[datetime, date, str, int, float] = None,
|
||||
forecast_start: Union[datetime, date, str, int, float, None] = None,
|
||||
prediction_hours: Optional[int] = None,
|
||||
) -> None:
|
||||
"""Processes the forecast data from the provided source (in-memory `data`, `filepath`, or `url`).
|
||||
@@ -368,6 +367,7 @@ class PVForecast:
|
||||
) # Invalid path
|
||||
else:
|
||||
raise ValueError("No prediction input data available.")
|
||||
assert data is not None # make mypy happy
|
||||
# Validate input data to be of a known format
|
||||
data_format = validate_pv_forecast_data(data)
|
||||
if data_format != "Akkudoktor":
|
||||
@@ -390,7 +390,7 @@ class PVForecast:
|
||||
# --------------------------------------------
|
||||
# From here Akkudoktor PV forecast data format
|
||||
# ---------------------------------------------
|
||||
self.meta = data.get("meta")
|
||||
self.meta = data.get("meta", {})
|
||||
all_values = data.get("values")
|
||||
|
||||
# timezone of the PV system
|
||||
@@ -454,7 +454,7 @@ class PVForecast:
|
||||
self._forecast_start = self.forecast_data[0].get_date_time()
|
||||
logger.debug(f"Forecast start adapted to {self._forecast_start}")
|
||||
|
||||
def load_data_from_file(self, filepath: str) -> dict:
|
||||
def load_data_from_file(self, filepath: str | Path) -> dict[str, Any]:
|
||||
"""Loads forecast data from a file.
|
||||
|
||||
Args:
|
||||
@@ -467,7 +467,7 @@ class PVForecast:
|
||||
data = json.load(file)
|
||||
return data
|
||||
|
||||
def load_data_from_url(self, url: str) -> dict:
|
||||
def load_data_from_url(self, url: str) -> dict[str, Any]:
|
||||
"""Loads forecast data from a URL.
|
||||
|
||||
Example:
|
||||
@@ -488,7 +488,7 @@ class PVForecast:
|
||||
return data
|
||||
|
||||
@cache_in_file() # use binary mode by default as we have python objects not text
|
||||
def load_data_from_url_with_caching(self, url: str, until_date=None) -> dict:
|
||||
def load_data_from_url_with_caching(self, url: str) -> dict[str, Any]:
|
||||
"""Loads data from a URL or from the cache if available.
|
||||
|
||||
Args:
|
||||
@@ -506,7 +506,7 @@ class PVForecast:
|
||||
logger.error(data)
|
||||
return data
|
||||
|
||||
def get_forecast_data(self):
|
||||
def get_forecast_data(self) -> list[ForecastData]:
|
||||
"""Returns the forecast data.
|
||||
|
||||
Returns:
|
||||
@@ -516,7 +516,7 @@ class PVForecast:
|
||||
|
||||
def get_temperature_forecast_for_date(
|
||||
self, input_date: Union[datetime, date, str, int, float, None]
|
||||
):
|
||||
) -> np.ndarray:
|
||||
"""Returns the temperature forecast for a specific date.
|
||||
|
||||
Args:
|
||||
@@ -543,7 +543,7 @@ class PVForecast:
|
||||
self,
|
||||
start_date: Union[datetime, date, str, int, float, None],
|
||||
end_date: Union[datetime, date, str, int, float, None],
|
||||
):
|
||||
) -> np.ndarray:
|
||||
"""Returns the PV forecast for a date range.
|
||||
|
||||
Args:
|
||||
@@ -575,7 +575,7 @@ class PVForecast:
|
||||
self,
|
||||
start_date: Union[datetime, date, str, int, float, None],
|
||||
end_date: Union[datetime, date, str, int, float, None],
|
||||
):
|
||||
) -> np.ndarray:
|
||||
"""Returns the temperature forecast for a given date range.
|
||||
|
||||
Args:
|
||||
@@ -601,7 +601,7 @@ class PVForecast:
|
||||
temperature_forecast = [data.get_temperature() for data in date_range_forecast]
|
||||
return np.array(temperature_forecast)[: self.prediction_hours]
|
||||
|
||||
def get_forecast_dataframe(self):
|
||||
def get_forecast_dataframe(self) -> pd.DataFrame:
|
||||
"""Converts the forecast data into a Pandas DataFrame.
|
||||
|
||||
Returns:
|
||||
@@ -623,7 +623,7 @@ class PVForecast:
|
||||
df = pd.DataFrame(data)
|
||||
return df
|
||||
|
||||
def get_forecast_start(self) -> datetime:
|
||||
def get_forecast_start(self) -> Optional[datetime]:
|
||||
"""Return the start of the forecast data in local timezone.
|
||||
|
||||
Returns:
|
||||
@@ -678,5 +678,5 @@ if __name__ == "__main__":
|
||||
"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.update_ac_power_measurement(ac_power_measurement=1000, date_time=datetime.now())
|
||||
print(forecast.report_ac_power_and_measurement())
|
||||
|
Reference in New Issue
Block a user