mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2025-09-20 02:31:14 +00:00
Nested config, devices registry
* All config now nested. - Use default config from model field default values. If providers should be enabled by default, non-empty default config file could be provided again. - Environment variable support with EOS_ prefix and __ between levels, e.g. EOS_SERVER__EOS_SERVER_PORT=8503 where all values are case insensitive. For more information see: https://docs.pydantic.dev/latest/concepts/pydantic_settings/#parsing-environment-variable-values - Use devices as registry for configured devices. DeviceBase as base class with for now just initializion support (in the future expand to operations during optimization). - Strip down ConfigEOS to the only configuration instance. Reload from file or reset to defaults is possible. * Fix multi-initialization of derived SingletonMixin classes.
This commit is contained in:
@@ -3,6 +3,7 @@ from typing import Optional
|
||||
from pydantic import Field
|
||||
|
||||
from akkudoktoreos.config.configabc import SettingsBaseModel
|
||||
from akkudoktoreos.prediction.elecpriceimport import ElecPriceImportCommonSettings
|
||||
|
||||
|
||||
class ElecPriceCommonSettings(SettingsBaseModel):
|
||||
@@ -12,3 +13,5 @@ class ElecPriceCommonSettings(SettingsBaseModel):
|
||||
elecprice_charges_kwh: Optional[float] = Field(
|
||||
default=None, ge=0, description="Electricity price charges (€/kWh)."
|
||||
)
|
||||
|
||||
provider_settings: Optional[ElecPriceImportCommonSettings] = None
|
||||
|
@@ -71,4 +71,4 @@ class ElecPriceProvider(PredictionProvider):
|
||||
return "ElecPriceProvider"
|
||||
|
||||
def enabled(self) -> bool:
|
||||
return self.provider_id() == self.config.elecprice_provider
|
||||
return self.provider_id() == self.config.elecprice.elecprice_provider
|
||||
|
@@ -108,13 +108,13 @@ class ElecPriceAkkudoktor(ElecPriceProvider):
|
||||
# Try to take data from 5 weeks back for prediction
|
||||
date = to_datetime(self.start_datetime - to_duration("35 days"), as_string="YYYY-MM-DD")
|
||||
last_date = to_datetime(self.end_datetime, as_string="YYYY-MM-DD")
|
||||
url = f"{source}/prices?start={date}&end={last_date}&tz={self.config.timezone}"
|
||||
url = f"{source}/prices?start={date}&end={last_date}&tz={self.config.prediction.timezone}"
|
||||
response = requests.get(url)
|
||||
logger.debug(f"Response from {url}: {response}")
|
||||
response.raise_for_status() # Raise an error for bad responses
|
||||
akkudoktor_data = self._validate_data(response.content)
|
||||
# We are working on fresh data (no cache), report update time
|
||||
self.update_datetime = to_datetime(in_timezone=self.config.timezone)
|
||||
self.update_datetime = to_datetime(in_timezone=self.config.prediction.timezone)
|
||||
return akkudoktor_data
|
||||
|
||||
def _cap_outliers(self, data: np.ndarray, sigma: int = 2) -> np.ndarray:
|
||||
@@ -156,13 +156,13 @@ class ElecPriceAkkudoktor(ElecPriceProvider):
|
||||
# in ascending order and have the same timestamps.
|
||||
|
||||
# Get elecprice_charges_kwh in wh
|
||||
charges_wh = (self.config.elecprice_charges_kwh or 0) / 1000
|
||||
charges_wh = (self.config.elecprice.elecprice_charges_kwh or 0) / 1000
|
||||
|
||||
highest_orig_datetime = None # newest datetime from the api after that we want to update.
|
||||
series_data = pd.Series(dtype=float) # Initialize an empty series
|
||||
|
||||
for value in akkudoktor_data.values:
|
||||
orig_datetime = to_datetime(value.start, in_timezone=self.config.timezone)
|
||||
orig_datetime = to_datetime(value.start, in_timezone=self.config.prediction.timezone)
|
||||
if highest_orig_datetime is None or orig_datetime > highest_orig_datetime:
|
||||
highest_orig_datetime = orig_datetime
|
||||
|
||||
@@ -184,14 +184,14 @@ class ElecPriceAkkudoktor(ElecPriceProvider):
|
||||
|
||||
# some of our data is already in the future, so we need to predict less. If we got less data we increase the prediction hours
|
||||
needed_prediction_hours = int(
|
||||
self.config.prediction_hours
|
||||
self.config.prediction.prediction_hours
|
||||
- ((highest_orig_datetime - self.start_datetime).total_seconds() // 3600)
|
||||
)
|
||||
|
||||
if needed_prediction_hours <= 0:
|
||||
logger.warning(
|
||||
f"No prediction needed. needed_prediction_hours={needed_prediction_hours}, prediction_hours={self.config.prediction_hours},highest_orig_datetime {highest_orig_datetime}, start_datetime {self.start_datetime}"
|
||||
) # this might keep data longer than self.start_datetime + self.config.prediction_hours in the records
|
||||
f"No prediction needed. needed_prediction_hours={needed_prediction_hours}, prediction_hours={self.config.prediction.prediction_hours},highest_orig_datetime {highest_orig_datetime}, start_datetime {self.start_datetime}"
|
||||
) # this might keep data longer than self.start_datetime + self.config.prediction.prediction_hours in the records
|
||||
return
|
||||
|
||||
if amount_datasets > 800: # we do the full ets with seasons of 1 week
|
||||
|
@@ -62,7 +62,12 @@ class ElecPriceImport(ElecPriceProvider, PredictionImportProvider):
|
||||
return "ElecPriceImport"
|
||||
|
||||
def _update_data(self, force_update: Optional[bool] = False) -> None:
|
||||
if self.config.elecpriceimport_file_path is not None:
|
||||
self.import_from_file(self.config.elecpriceimport_file_path, key_prefix="elecprice")
|
||||
if self.config.elecpriceimport_json is not None:
|
||||
self.import_from_json(self.config.elecpriceimport_json, key_prefix="elecprice")
|
||||
if self.config.elecprice.provider_settings.elecpriceimport_file_path is not None:
|
||||
self.import_from_file(
|
||||
self.config.elecprice.provider_settings.elecpriceimport_file_path,
|
||||
key_prefix="elecprice",
|
||||
)
|
||||
if self.config.elecprice.provider_settings.elecpriceimport_json is not None:
|
||||
self.import_from_json(
|
||||
self.config.elecprice.provider_settings.elecpriceimport_json, key_prefix="elecprice"
|
||||
)
|
||||
|
@@ -6,6 +6,8 @@ from pathlib import Path
|
||||
import numpy as np
|
||||
from scipy.interpolate import RegularGridInterpolator
|
||||
|
||||
from akkudoktoreos.core.coreabc import SingletonMixin
|
||||
|
||||
|
||||
class SelfConsumptionProbabilityInterpolator:
|
||||
def __init__(self, filepath: str | Path):
|
||||
@@ -67,5 +69,17 @@ class SelfConsumptionProbabilityInterpolator:
|
||||
# return self_consumption_rate
|
||||
|
||||
|
||||
# Test the function
|
||||
# print(calculate_self_consumption(1000, 1200))
|
||||
class EOSLoadInterpolator(SelfConsumptionProbabilityInterpolator, SingletonMixin):
|
||||
def __init__(self) -> None:
|
||||
if hasattr(self, "_initialized"):
|
||||
return
|
||||
filename = Path(__file__).parent.resolve() / ".." / "data" / "regular_grid_interpolator.pkl"
|
||||
super().__init__(filename)
|
||||
|
||||
|
||||
# Initialize the Energy Management System, it is a singleton.
|
||||
eos_load_interpolator = EOSLoadInterpolator()
|
||||
|
||||
|
||||
def get_eos_load_interpolator() -> EOSLoadInterpolator:
|
||||
return eos_load_interpolator
|
||||
|
@@ -1,11 +1,13 @@
|
||||
"""Load forecast module for load predictions."""
|
||||
|
||||
from typing import Optional
|
||||
from typing import Optional, Union
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from akkudoktoreos.config.configabc import SettingsBaseModel
|
||||
from akkudoktoreos.core.logging import get_logger
|
||||
from akkudoktoreos.prediction.loadakkudoktor import LoadAkkudoktorCommonSettings
|
||||
from akkudoktoreos.prediction.loadimport import LoadImportCommonSettings
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -16,3 +18,7 @@ class LoadCommonSettings(SettingsBaseModel):
|
||||
load_provider: Optional[str] = Field(
|
||||
default=None, description="Load provider id of provider to be used."
|
||||
)
|
||||
|
||||
provider_settings: Optional[Union[LoadAkkudoktorCommonSettings, LoadImportCommonSettings]] = (
|
||||
None
|
||||
)
|
||||
|
@@ -58,4 +58,4 @@ class LoadProvider(PredictionProvider):
|
||||
return "LoadProvider"
|
||||
|
||||
def enabled(self) -> bool:
|
||||
return self.provider_id() == self.config.load_provider
|
||||
return self.provider_id() == self.config.load.load_provider
|
||||
|
@@ -91,7 +91,9 @@ class LoadAkkudoktor(LoadProvider):
|
||||
list(zip(file_data["yearly_profiles"], file_data["yearly_profiles_std"]))
|
||||
)
|
||||
# Calculate values in W by relative profile data and yearly consumption given in kWh
|
||||
data_year_energy = profile_data * self.config.loadakkudoktor_year_energy * 1000
|
||||
data_year_energy = (
|
||||
profile_data * self.config.load.provider_settings.loadakkudoktor_year_energy * 1000
|
||||
)
|
||||
except FileNotFoundError:
|
||||
error_msg = f"Error: File {load_file} not found."
|
||||
logger.error(error_msg)
|
||||
@@ -109,7 +111,7 @@ class LoadAkkudoktor(LoadProvider):
|
||||
# We provide prediction starting at start of day, to be compatible to old system.
|
||||
# End date for prediction is prediction hours from now.
|
||||
date = self.start_datetime.start_of("day")
|
||||
end_date = self.start_datetime.add(hours=self.config.prediction_hours)
|
||||
end_date = self.start_datetime.add(hours=self.config.prediction.prediction_hours)
|
||||
while compare_datetimes(date, end_date).lt:
|
||||
# Extract mean (index 0) and standard deviation (index 1) for the given day and hour
|
||||
# Day indexing starts at 0, -1 because of that
|
||||
@@ -127,4 +129,4 @@ class LoadAkkudoktor(LoadProvider):
|
||||
self.update_value(date, values)
|
||||
date += to_duration("1 hour")
|
||||
# We are working on fresh data (no cache), report update time
|
||||
self.update_datetime = to_datetime(in_timezone=self.config.timezone)
|
||||
self.update_datetime = to_datetime(in_timezone=self.config.prediction.timezone)
|
||||
|
@@ -58,7 +58,11 @@ class LoadImport(LoadProvider, PredictionImportProvider):
|
||||
return "LoadImport"
|
||||
|
||||
def _update_data(self, force_update: Optional[bool] = False) -> None:
|
||||
if self.config.load_import_file_path is not None:
|
||||
self.import_from_file(self.config.load_import_file_path, key_prefix="load")
|
||||
if self.config.load_import_json is not None:
|
||||
self.import_from_json(self.config.load_import_json, key_prefix="load")
|
||||
if self.config.load.provider_settings.load_import_file_path is not None:
|
||||
self.import_from_file(
|
||||
self.config.provider_settings.load_import_file_path, key_prefix="load"
|
||||
)
|
||||
if self.config.load.provider_settings.load_import_json is not None:
|
||||
self.import_from_json(
|
||||
self.config.load.provider_settings.load_import_json, key_prefix="load"
|
||||
)
|
||||
|
@@ -80,13 +80,13 @@ class PredictionCommonSettings(SettingsBaseModel):
|
||||
description="Number of hours into the past for historical predictions data",
|
||||
)
|
||||
latitude: Optional[float] = Field(
|
||||
default=None,
|
||||
default=52.52,
|
||||
ge=-90.0,
|
||||
le=90.0,
|
||||
description="Latitude in decimal degrees, between -90 and 90, north is positive (ISO 19115) (°)",
|
||||
)
|
||||
longitude: Optional[float] = Field(
|
||||
default=None,
|
||||
default=13.405,
|
||||
ge=-180.0,
|
||||
le=180.0,
|
||||
description="Longitude in decimal degrees, within -180 to 180 (°)",
|
||||
|
@@ -121,9 +121,9 @@ class PredictionStartEndKeepMixin(PredictionBase):
|
||||
Returns:
|
||||
Optional[DateTime]: The calculated end datetime, or `None` if inputs are missing.
|
||||
"""
|
||||
if self.start_datetime and self.config.prediction_hours:
|
||||
if self.start_datetime and self.config.prediction.prediction_hours:
|
||||
end_datetime = self.start_datetime + to_duration(
|
||||
f"{self.config.prediction_hours} hours"
|
||||
f"{self.config.prediction.prediction_hours} hours"
|
||||
)
|
||||
dst_change = end_datetime.offset_hours - self.start_datetime.offset_hours
|
||||
logger.debug(f"Pre: {self.start_datetime}..{end_datetime}: DST change: {dst_change}")
|
||||
@@ -147,10 +147,10 @@ class PredictionStartEndKeepMixin(PredictionBase):
|
||||
return None
|
||||
historic_hours = self.historic_hours_min()
|
||||
if (
|
||||
self.config.prediction_historic_hours
|
||||
and self.config.prediction_historic_hours > historic_hours
|
||||
self.config.prediction.prediction_historic_hours
|
||||
and self.config.prediction.prediction_historic_hours > historic_hours
|
||||
):
|
||||
historic_hours = int(self.config.prediction_historic_hours)
|
||||
historic_hours = int(self.config.prediction.prediction_historic_hours)
|
||||
return self.start_datetime - to_duration(f"{historic_hours} hours")
|
||||
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
|
@@ -6,6 +6,7 @@ from pydantic import Field, computed_field
|
||||
|
||||
from akkudoktoreos.config.configabc import SettingsBaseModel
|
||||
from akkudoktoreos.core.logging import get_logger
|
||||
from akkudoktoreos.prediction.pvforecastimport import PVForecastImportCommonSettings
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -260,7 +261,7 @@ class PVForecastCommonSettings(SettingsBaseModel):
|
||||
default=None, description="Nominal power of PV system in kW."
|
||||
)
|
||||
pvforecast4_pvtechchoice: Optional[str] = Field(
|
||||
"crystSi", description="PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'."
|
||||
default="crystSi", description="PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'."
|
||||
)
|
||||
pvforecast4_mountingplace: Optional[str] = Field(
|
||||
default="free",
|
||||
@@ -316,7 +317,7 @@ class PVForecastCommonSettings(SettingsBaseModel):
|
||||
default=None, description="Nominal power of PV system in kW."
|
||||
)
|
||||
pvforecast5_pvtechchoice: Optional[str] = Field(
|
||||
"crystSi", description="PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'."
|
||||
default="crystSi", description="PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'."
|
||||
)
|
||||
pvforecast5_mountingplace: Optional[str] = Field(
|
||||
default="free",
|
||||
@@ -359,6 +360,8 @@ class PVForecastCommonSettings(SettingsBaseModel):
|
||||
|
||||
pvforecast_max_planes: ClassVar[int] = 6 # Maximum number of planes that can be set
|
||||
|
||||
provider_settings: Optional[PVForecastImportCommonSettings] = None
|
||||
|
||||
# Computed fields
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
|
@@ -54,6 +54,6 @@ class PVForecastProvider(PredictionProvider):
|
||||
|
||||
def enabled(self) -> bool:
|
||||
logger.debug(
|
||||
f"PVForecastProvider ID {self.provider_id()} vs. config {self.config.pvforecast_provider}"
|
||||
f"PVForecastProvider ID {self.provider_id()} vs. config {self.config.pvforecast.pvforecast_provider}"
|
||||
)
|
||||
return self.provider_id() == self.config.pvforecast_provider
|
||||
return self.provider_id() == self.config.pvforecast.pvforecast_provider
|
||||
|
@@ -203,19 +203,23 @@ class PVForecastAkkudoktor(PVForecastProvider):
|
||||
"""Build akkudoktor.net API request URL."""
|
||||
base_url = "https://api.akkudoktor.net/forecast"
|
||||
query_params = [
|
||||
f"lat={self.config.latitude}",
|
||||
f"lon={self.config.longitude}",
|
||||
f"lat={self.config.prediction.latitude}",
|
||||
f"lon={self.config.prediction.longitude}",
|
||||
]
|
||||
|
||||
for i in range(len(self.config.pvforecast_planes)):
|
||||
query_params.append(f"power={int(self.config.pvforecast_planes_peakpower[i] * 1000)}")
|
||||
query_params.append(f"azimuth={int(self.config.pvforecast_planes_azimuth[i])}")
|
||||
query_params.append(f"tilt={int(self.config.pvforecast_planes_tilt[i])}")
|
||||
for i in range(len(self.config.pvforecast.pvforecast_planes)):
|
||||
query_params.append(
|
||||
f"powerInverter={int(self.config.pvforecast_planes_inverter_paco[i])}"
|
||||
f"power={int(self.config.pvforecast.pvforecast_planes_peakpower[i] * 1000)}"
|
||||
)
|
||||
query_params.append(
|
||||
f"azimuth={int(self.config.pvforecast.pvforecast_planes_azimuth[i])}"
|
||||
)
|
||||
query_params.append(f"tilt={int(self.config.pvforecast.pvforecast_planes_tilt[i])}")
|
||||
query_params.append(
|
||||
f"powerInverter={int(self.config.pvforecast.pvforecast_planes_inverter_paco[i])}"
|
||||
)
|
||||
horizon_values = ",".join(
|
||||
str(int(h)) for h in self.config.pvforecast_planes_userhorizon[i]
|
||||
str(int(h)) for h in self.config.pvforecast.pvforecast_planes_userhorizon[i]
|
||||
)
|
||||
query_params.append(f"horizont={horizon_values}")
|
||||
|
||||
@@ -226,7 +230,7 @@ class PVForecastAkkudoktor(PVForecastProvider):
|
||||
"cellCoEff=-0.36",
|
||||
"inverterEfficiency=0.8",
|
||||
"albedo=0.25",
|
||||
f"timezone={self.config.timezone}",
|
||||
f"timezone={self.config.prediction.timezone}",
|
||||
"hourly=relativehumidity_2m%2Cwindspeed_10m",
|
||||
]
|
||||
)
|
||||
@@ -255,7 +259,7 @@ class PVForecastAkkudoktor(PVForecastProvider):
|
||||
logger.debug(f"Response from {self._url()}: {response}")
|
||||
akkudoktor_data = self._validate_data(response.content)
|
||||
# We are working on fresh data (no cache), report update time
|
||||
self.update_datetime = to_datetime(in_timezone=self.config.timezone)
|
||||
self.update_datetime = to_datetime(in_timezone=self.config.prediction.timezone)
|
||||
return akkudoktor_data
|
||||
|
||||
def _update_data(self, force_update: Optional[bool] = False) -> None:
|
||||
@@ -265,7 +269,7 @@ class PVForecastAkkudoktor(PVForecastProvider):
|
||||
`PVForecastAkkudoktorDataRecord`.
|
||||
"""
|
||||
# Assure we have something to request PV power for.
|
||||
if not self.config.pvforecast_planes:
|
||||
if not self.config.pvforecast.pvforecast_planes:
|
||||
# No planes for PV
|
||||
error_msg = "Requested PV forecast, but no planes configured."
|
||||
logger.error(f"Configuration error: {error_msg}")
|
||||
@@ -275,17 +279,17 @@ class PVForecastAkkudoktor(PVForecastProvider):
|
||||
akkudoktor_data = self._request_forecast(force_update=force_update) # type: ignore
|
||||
|
||||
# Timezone of the PV system
|
||||
if self.config.timezone != akkudoktor_data.meta.timezone:
|
||||
error_msg = f"Configured timezone '{self.config.timezone}' does not match Akkudoktor timezone '{akkudoktor_data.meta.timezone}'."
|
||||
if self.config.prediction.timezone != akkudoktor_data.meta.timezone:
|
||||
error_msg = f"Configured timezone '{self.config.prediction.timezone}' does not match Akkudoktor timezone '{akkudoktor_data.meta.timezone}'."
|
||||
logger.error(f"Akkudoktor schema change: {error_msg}")
|
||||
raise ValueError(error_msg)
|
||||
|
||||
# Assumption that all lists are the same length and are ordered chronologically
|
||||
# in ascending order and have the same timestamps.
|
||||
if len(akkudoktor_data.values[0]) < self.config.prediction_hours:
|
||||
if len(akkudoktor_data.values[0]) < self.config.prediction.prediction_hours:
|
||||
# Expect one value set per prediction hour
|
||||
error_msg = (
|
||||
f"The forecast must cover at least {self.config.prediction_hours} hours, "
|
||||
f"The forecast must cover at least {self.config.prediction.prediction_hours} hours, "
|
||||
f"but only {len(akkudoktor_data.values[0])} data sets are given in forecast data."
|
||||
)
|
||||
logger.error(f"Akkudoktor schema change: {error_msg}")
|
||||
@@ -296,7 +300,7 @@ class PVForecastAkkudoktor(PVForecastProvider):
|
||||
# Iterate over forecast data points
|
||||
for forecast_values in zip(*akkudoktor_data.values):
|
||||
original_datetime = forecast_values[0].datetime
|
||||
dt = to_datetime(original_datetime, in_timezone=self.config.timezone)
|
||||
dt = to_datetime(original_datetime, in_timezone=self.config.prediction.timezone)
|
||||
|
||||
# Skip outdated forecast data
|
||||
if compare_datetimes(dt, self.start_datetime.start_of("day")).lt:
|
||||
@@ -314,9 +318,9 @@ class PVForecastAkkudoktor(PVForecastProvider):
|
||||
|
||||
self.update_value(dt, data)
|
||||
|
||||
if len(self) < self.config.prediction_hours:
|
||||
if len(self) < self.config.prediction.prediction_hours:
|
||||
raise ValueError(
|
||||
f"The forecast must cover at least {self.config.prediction_hours} hours, "
|
||||
f"The forecast must cover at least {self.config.prediction.prediction_hours} hours, "
|
||||
f"but only {len(self)} hours starting from {self.start_datetime} "
|
||||
f"were predicted."
|
||||
)
|
||||
@@ -365,31 +369,35 @@ if __name__ == "__main__":
|
||||
"""
|
||||
# Set up the configuration with necessary fields for URL generation
|
||||
settings_data = {
|
||||
"prediction_hours": 48,
|
||||
"prediction_historic_hours": 24,
|
||||
"latitude": 52.52,
|
||||
"longitude": 13.405,
|
||||
"pvforecast_provider": "PVForecastAkkudoktor",
|
||||
"pvforecast0_peakpower": 5.0,
|
||||
"pvforecast0_surface_azimuth": -10,
|
||||
"pvforecast0_surface_tilt": 7,
|
||||
"pvforecast0_userhorizon": [20, 27, 22, 20],
|
||||
"pvforecast0_inverter_paco": 10000,
|
||||
"pvforecast1_peakpower": 4.8,
|
||||
"pvforecast1_surface_azimuth": -90,
|
||||
"pvforecast1_surface_tilt": 7,
|
||||
"pvforecast1_userhorizon": [30, 30, 30, 50],
|
||||
"pvforecast1_inverter_paco": 10000,
|
||||
"pvforecast2_peakpower": 1.4,
|
||||
"pvforecast2_surface_azimuth": -40,
|
||||
"pvforecast2_surface_tilt": 60,
|
||||
"pvforecast2_userhorizon": [60, 30, 0, 30],
|
||||
"pvforecast2_inverter_paco": 2000,
|
||||
"pvforecast3_peakpower": 1.6,
|
||||
"pvforecast3_surface_azimuth": 5,
|
||||
"pvforecast3_surface_tilt": 45,
|
||||
"pvforecast3_userhorizon": [45, 25, 30, 60],
|
||||
"pvforecast3_inverter_paco": 1400,
|
||||
"prediction": {
|
||||
"prediction_hours": 48,
|
||||
"prediction_historic_hours": 24,
|
||||
"latitude": 52.52,
|
||||
"longitude": 13.405,
|
||||
},
|
||||
"pvforecast": {
|
||||
"pvforecast_provider": "PVForecastAkkudoktor",
|
||||
"pvforecast0_peakpower": 5.0,
|
||||
"pvforecast0_surface_azimuth": -10,
|
||||
"pvforecast0_surface_tilt": 7,
|
||||
"pvforecast0_userhorizon": [20, 27, 22, 20],
|
||||
"pvforecast0_inverter_paco": 10000,
|
||||
"pvforecast1_peakpower": 4.8,
|
||||
"pvforecast1_surface_azimuth": -90,
|
||||
"pvforecast1_surface_tilt": 7,
|
||||
"pvforecast1_userhorizon": [30, 30, 30, 50],
|
||||
"pvforecast1_inverter_paco": 10000,
|
||||
"pvforecast2_peakpower": 1.4,
|
||||
"pvforecast2_surface_azimuth": -40,
|
||||
"pvforecast2_surface_tilt": 60,
|
||||
"pvforecast2_userhorizon": [60, 30, 0, 30],
|
||||
"pvforecast2_inverter_paco": 2000,
|
||||
"pvforecast3_peakpower": 1.6,
|
||||
"pvforecast3_surface_azimuth": 5,
|
||||
"pvforecast3_surface_tilt": 45,
|
||||
"pvforecast3_userhorizon": [45, 25, 30, 60],
|
||||
"pvforecast3_inverter_paco": 1400,
|
||||
},
|
||||
}
|
||||
|
||||
# Initialize the forecast object with the generated configuration
|
||||
|
@@ -62,7 +62,13 @@ class PVForecastImport(PVForecastProvider, PredictionImportProvider):
|
||||
return "PVForecastImport"
|
||||
|
||||
def _update_data(self, force_update: Optional[bool] = False) -> None:
|
||||
if self.config.pvforecastimport_file_path is not None:
|
||||
self.import_from_file(self.config.pvforecastimport_file_path, key_prefix="pvforecast")
|
||||
if self.config.pvforecastimport_json is not None:
|
||||
self.import_from_json(self.config.pvforecastimport_json, key_prefix="pvforecast")
|
||||
if self.config.pvforecast.provider_settings.pvforecastimport_file_path is not None:
|
||||
self.import_from_file(
|
||||
self.config.pvforecast.provider_settings.pvforecastimport_file_path,
|
||||
key_prefix="pvforecast",
|
||||
)
|
||||
if self.config.pvforecast.provider_settings.pvforecastimport_json is not None:
|
||||
self.import_from_json(
|
||||
self.config.pvforecast.provider_settings.pvforecastimport_json,
|
||||
key_prefix="pvforecast",
|
||||
)
|
||||
|
@@ -5,9 +5,12 @@ from typing import Optional
|
||||
from pydantic import Field
|
||||
|
||||
from akkudoktoreos.config.configabc import SettingsBaseModel
|
||||
from akkudoktoreos.prediction.weatherimport import WeatherImportCommonSettings
|
||||
|
||||
|
||||
class WeatherCommonSettings(SettingsBaseModel):
|
||||
weather_provider: Optional[str] = Field(
|
||||
default=None, description="Weather provider id of provider to be used."
|
||||
)
|
||||
|
||||
provider_settings: Optional[WeatherImportCommonSettings] = None
|
||||
|
@@ -126,7 +126,7 @@ class WeatherProvider(PredictionProvider):
|
||||
return "WeatherProvider"
|
||||
|
||||
def enabled(self) -> bool:
|
||||
return self.provider_id() == self.config.weather_provider
|
||||
return self.provider_id() == self.config.weather.weather_provider
|
||||
|
||||
@classmethod
|
||||
def estimate_irradiance_from_cloud_cover(
|
||||
|
@@ -99,7 +99,7 @@ class WeatherBrightSky(WeatherProvider):
|
||||
date = to_datetime(self.start_datetime, as_string="YYYY-MM-DD")
|
||||
last_date = to_datetime(self.end_datetime, as_string="YYYY-MM-DD")
|
||||
response = requests.get(
|
||||
f"{source}/weather?lat={self.config.latitude}&lon={self.config.longitude}&date={date}&last_date={last_date}&tz={self.config.timezone}"
|
||||
f"{source}/weather?lat={self.config.prediction.latitude}&lon={self.config.prediction.longitude}&date={date}&last_date={last_date}&tz={self.config.prediction.timezone}"
|
||||
)
|
||||
response.raise_for_status() # Raise an error for bad responses
|
||||
logger.debug(f"Response from {source}: {response}")
|
||||
@@ -109,7 +109,7 @@ class WeatherBrightSky(WeatherProvider):
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
# We are working on fresh data (no cache), report update time
|
||||
self.update_datetime = to_datetime(in_timezone=self.config.timezone)
|
||||
self.update_datetime = to_datetime(in_timezone=self.config.prediction.timezone)
|
||||
return brightsky_data
|
||||
|
||||
def _description_to_series(self, description: str) -> pd.Series:
|
||||
@@ -200,7 +200,7 @@ class WeatherBrightSky(WeatherProvider):
|
||||
description = "Total Clouds (% Sky Obscured)"
|
||||
cloud_cover = self._description_to_series(description)
|
||||
ghi, dni, dhi = self.estimate_irradiance_from_cloud_cover(
|
||||
self.config.latitude, self.config.longitude, cloud_cover
|
||||
self.config.prediction.latitude, self.config.prediction.longitude, cloud_cover
|
||||
)
|
||||
|
||||
description = "Global Horizontal Irradiance (W/m2)"
|
||||
|
@@ -91,13 +91,13 @@ class WeatherClearOutside(WeatherProvider):
|
||||
response: Weather forecast request reponse from ClearOutside.
|
||||
"""
|
||||
source = "https://clearoutside.com/forecast"
|
||||
latitude = round(self.config.latitude, 2)
|
||||
longitude = round(self.config.longitude, 2)
|
||||
latitude = round(self.config.prediction.latitude, 2)
|
||||
longitude = round(self.config.prediction.longitude, 2)
|
||||
response = requests.get(f"{source}/{latitude}/{longitude}?desktop=true")
|
||||
response.raise_for_status() # Raise an error for bad responses
|
||||
logger.debug(f"Response from {source}: {response}")
|
||||
# We are working on fresh data (no cache), report update time
|
||||
self.update_datetime = to_datetime(in_timezone=self.config.timezone)
|
||||
self.update_datetime = to_datetime(in_timezone=self.config.prediction.timezone)
|
||||
return response
|
||||
|
||||
def _update_data(self, force_update: Optional[bool] = None) -> None:
|
||||
@@ -307,7 +307,7 @@ class WeatherClearOutside(WeatherProvider):
|
||||
data=clearout_data["Total Clouds (% Sky Obscured)"], index=clearout_data["DateTime"]
|
||||
)
|
||||
ghi, dni, dhi = self.estimate_irradiance_from_cloud_cover(
|
||||
self.config.latitude, self.config.longitude, cloud_cover
|
||||
self.config.prediction.latitude, self.config.prediction.longitude, cloud_cover
|
||||
)
|
||||
|
||||
# Add GHI, DNI, DHI to clearout data
|
||||
|
@@ -59,7 +59,11 @@ class WeatherImport(WeatherProvider, PredictionImportProvider):
|
||||
return "WeatherImport"
|
||||
|
||||
def _update_data(self, force_update: Optional[bool] = False) -> None:
|
||||
if self.config.weatherimport_file_path is not None:
|
||||
self.import_from_file(self.config.weatherimport_file_path, key_prefix="weather")
|
||||
if self.config.weatherimport_json is not None:
|
||||
self.import_from_json(self.config.weatherimport_json, key_prefix="weather")
|
||||
if self.config.weather.provider_settings.weatherimport_file_path is not None:
|
||||
self.import_from_file(
|
||||
self.config.weather.provider_settings.weatherimport_file_path, key_prefix="weather"
|
||||
)
|
||||
if self.config.weather.provider_settings.weatherimport_json is not None:
|
||||
self.import_from_json(
|
||||
self.config.weather.provider_settings.weatherimport_json, key_prefix="weather"
|
||||
)
|
||||
|
Reference in New Issue
Block a user