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:
Dominique Lasserre
2025-01-12 05:19:37 +01:00
parent f09658578a
commit be26457563
72 changed files with 1297 additions and 1712 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"
)

View File

@@ -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

View File

@@ -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
)

View File

@@ -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

View File

@@ -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)

View File

@@ -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"
)

View File

@@ -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 (°)",

View File

@@ -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]

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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",
)

View File

@@ -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

View File

@@ -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(

View File

@@ -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)"

View File

@@ -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

View File

@@ -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"
)