Config: Move lat/long/timezone from prediction to general

This commit is contained in:
Dominique Lasserre 2025-01-20 22:58:59 +01:00
parent 1658b491d2
commit c1dd31528b
19 changed files with 297 additions and 316 deletions

View File

@ -2,7 +2,22 @@
## Settings for common configuration
General configuration to set directories of cache and output files.
General configuration to set directories of cache and output files and system location (latitude
and longitude).
Validators ensure each parameter is within a specified range. A computed property, `timezone`,
determines the time zone based on latitude and longitude.
Attributes:
latitude (Optional[float]): Latitude in degrees, must be between -90 and 90.
longitude (Optional[float]): Longitude in degrees, must be between -180 and 180.
Properties:
timezone (Optional[str]): Computed time zone string based on the specified latitude
and longitude.
Validators:
validate_latitude (float): Ensures `latitude` is within the range -90 to 90.
validate_longitude (float): Ensures `longitude` is within the range -180 to 180.
:::{table} general
:widths: 10 20 10 5 5 30
@ -13,6 +28,9 @@ General configuration to set directories of cache and output files.
| data_folder_path | `EOS_GENERAL__DATA_FOLDER_PATH` | `Optional[pathlib.Path]` | `rw` | `None` | Path to EOS data directory. |
| data_output_subpath | `EOS_GENERAL__DATA_OUTPUT_SUBPATH` | `Optional[pathlib.Path]` | `rw` | `output` | Sub-path for the EOS output data directory. |
| data_cache_subpath | `EOS_GENERAL__DATA_CACHE_SUBPATH` | `Optional[pathlib.Path]` | `rw` | `cache` | Sub-path for the EOS cache data directory. |
| latitude | `EOS_GENERAL__LATITUDE` | `Optional[float]` | `rw` | `52.52` | Latitude in decimal degrees, between -90 and 90, north is positive (ISO 19115) (°) |
| longitude | `EOS_GENERAL__LONGITUDE` | `Optional[float]` | `rw` | `13.405` | Longitude in decimal degrees, within -180 to 180 (°) |
| timezone | | `Optional[str]` | `ro` | `N/A` | Compute timezone based on latitude and longitude. |
| data_output_path | | `Optional[pathlib.Path]` | `ro` | `N/A` | Compute data_output_path based on data_folder_path. |
| data_cache_path | | `Optional[pathlib.Path]` | `ro` | `N/A` | Compute data_cache_path based on data_folder_path. |
| config_folder_path | | `Optional[pathlib.Path]` | `ro` | `N/A` | Path to EOS configuration directory. |
@ -28,7 +46,9 @@ General configuration to set directories of cache and output files.
"general": {
"data_folder_path": null,
"data_output_subpath": "output",
"data_cache_subpath": "cache"
"data_cache_subpath": "cache",
"latitude": 52.52,
"longitude": 13.405
}
}
```
@ -43,6 +63,9 @@ General configuration to set directories of cache and output files.
"data_folder_path": null,
"data_output_subpath": "output",
"data_cache_subpath": "cache",
"latitude": 52.52,
"longitude": 13.405,
"timezone": "Europe/Berlin",
"data_output_path": null,
"data_cache_path": null,
"config_folder_path": "/home/user/.config/net.akkudoktoreos.net",
@ -308,27 +331,18 @@ Attributes:
## General Prediction Configuration
This class provides configuration for prediction settings, allowing users to specify
parameters such as the forecast duration (in hours) and location (latitude and longitude).
Validators ensure each parameter is within a specified range. A computed property, `timezone`,
determines the time zone based on latitude and longitude.
parameters such as the forecast duration (in hours).
Validators ensure each parameter is within a specified range.
Attributes:
hours (Optional[int]): Number of hours into the future for predictions.
Must be non-negative.
historic_hours (Optional[int]): Number of hours into the past for historical data.
Must be non-negative.
latitude (Optional[float]): Latitude in degrees, must be between -90 and 90.
longitude (Optional[float]): Longitude in degrees, must be between -180 and 180.
Properties:
timezone (Optional[str]): Computed time zone string based on the specified latitude
and longitude.
Validators:
validate_hours (int): Ensures `hours` is a non-negative integer.
validate_historic_hours (int): Ensures `historic_hours` is a non-negative integer.
validate_latitude (float): Ensures `latitude` is within the range -90 to 90.
validate_longitude (float): Ensures `longitude` is within the range -180 to 180.
:::{table} prediction
:widths: 10 20 10 5 5 30
@ -338,12 +352,9 @@ Validators:
| ---- | -------------------- | ---- | --------- | ------- | ----------- |
| hours | `EOS_PREDICTION__HOURS` | `Optional[int]` | `rw` | `48` | Number of hours into the future for predictions |
| historic_hours | `EOS_PREDICTION__HISTORIC_HOURS` | `Optional[int]` | `rw` | `48` | Number of hours into the past for historical predictions data |
| latitude | `EOS_PREDICTION__LATITUDE` | `Optional[float]` | `rw` | `52.52` | Latitude in decimal degrees, between -90 and 90, north is positive (ISO 19115) (°) |
| longitude | `EOS_PREDICTION__LONGITUDE` | `Optional[float]` | `rw` | `13.405` | Longitude in decimal degrees, within -180 to 180 (°) |
| timezone | | `Optional[str]` | `ro` | `N/A` | Compute timezone based on latitude and longitude. |
:::
### Example Input
### Example Input/Output
```{eval-rst}
.. code-block:: json
@ -351,25 +362,7 @@ Validators:
{
"prediction": {
"hours": 48,
"historic_hours": 48,
"latitude": 52.52,
"longitude": 13.405
}
}
```
### Example Output
```{eval-rst}
.. code-block:: json
{
"prediction": {
"hours": 48,
"historic_hours": 48,
"latitude": 52.52,
"longitude": 13.405,
"timezone": "Europe/Berlin"
"historic_hours": 48
}
}
```

View File

@ -295,8 +295,8 @@ The `PVForecastAkkudoktor` provider retrieves the PV power forecast data directl
The following prediction configuration options of the PV system must be set:
- `prediction.latitude`: Latitude in decimal degrees, between -90 and 90, north is positive (ISO 19115) (°)"
- `prediction.longitude`: Longitude in decimal degrees, within -180 to 180 (°)
- `general.latitude`: Latitude in decimal degrees, between -90 and 90, north is positive (ISO 19115) (°)"
- `general.longitude`: Longitude in decimal degrees, within -180 to 180 (°)
For each plane of the PV system the following configuration options must be set:
@ -310,7 +310,7 @@ Example:
```Python
{
"prediction": {
"general": {
"latitude": 50.1234,
"longitude": 9.7654,
},

View File

@ -107,7 +107,7 @@
"type": "object"
},
"ConfigCommonSettings-Input": {
"description": "Settings for common configuration.\n\nGeneral configuration to set directories of cache and output files.",
"description": "Settings for common configuration.\n\nGeneral configuration to set directories of cache and output files and system location (latitude\nand longitude).\nValidators ensure each parameter is within a specified range. A computed property, `timezone`,\ndetermines the time zone based on latitude and longitude.\n\nAttributes:\n latitude (Optional[float]): Latitude in degrees, must be between -90 and 90.\n longitude (Optional[float]): Longitude in degrees, must be between -180 and 180.\n\nProperties:\n timezone (Optional[str]): Computed time zone string based on the specified latitude\n and longitude.\n\nValidators:\n validate_latitude (float): Ensures `latitude` is within the range -90 to 90.\n validate_longitude (float): Ensures `longitude` is within the range -180 to 180.",
"properties": {
"data_cache_subpath": {
"anyOf": [
@ -153,13 +153,43 @@
"default": "output",
"description": "Sub-path for the EOS output data directory.",
"title": "Data Output Subpath"
},
"latitude": {
"anyOf": [
{
"maximum": 90.0,
"minimum": -90.0,
"type": "number"
},
{
"type": "null"
}
],
"default": 52.52,
"description": "Latitude in decimal degrees, between -90 and 90, north is positive (ISO 19115) (\u00b0)",
"title": "Latitude"
},
"longitude": {
"anyOf": [
{
"maximum": 180.0,
"minimum": -180.0,
"type": "number"
},
{
"type": "null"
}
],
"default": 13.405,
"description": "Longitude in decimal degrees, within -180 to 180 (\u00b0)",
"title": "Longitude"
}
},
"title": "ConfigCommonSettings",
"type": "object"
},
"ConfigCommonSettings-Output": {
"description": "Settings for common configuration.\n\nGeneral configuration to set directories of cache and output files.",
"description": "Settings for common configuration.\n\nGeneral configuration to set directories of cache and output files and system location (latitude\nand longitude).\nValidators ensure each parameter is within a specified range. A computed property, `timezone`,\ndetermines the time zone based on latitude and longitude.\n\nAttributes:\n latitude (Optional[float]): Latitude in degrees, must be between -90 and 90.\n longitude (Optional[float]): Longitude in degrees, must be between -180 and 180.\n\nProperties:\n timezone (Optional[str]): Computed time zone string based on the specified latitude\n and longitude.\n\nValidators:\n validate_latitude (float): Ensures `latitude` is within the range -90 to 90.\n validate_longitude (float): Ensures `longitude` is within the range -180 to 180.",
"properties": {
"config_file_path": {
"anyOf": [
@ -261,9 +291,53 @@
"default": "output",
"description": "Sub-path for the EOS output data directory.",
"title": "Data Output Subpath"
},
"latitude": {
"anyOf": [
{
"maximum": 90.0,
"minimum": -90.0,
"type": "number"
},
{
"type": "null"
}
],
"default": 52.52,
"description": "Latitude in decimal degrees, between -90 and 90, north is positive (ISO 19115) (\u00b0)",
"title": "Latitude"
},
"longitude": {
"anyOf": [
{
"maximum": 180.0,
"minimum": -180.0,
"type": "number"
},
{
"type": "null"
}
],
"default": 13.405,
"description": "Longitude in decimal degrees, within -180 to 180 (\u00b0)",
"title": "Longitude"
},
"timezone": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Compute timezone based on latitude and longitude.",
"readOnly": true,
"title": "Timezone"
}
},
"required": [
"timezone",
"data_output_path",
"data_cache_path",
"config_folder_path",
@ -290,7 +364,10 @@
"config_file_path": "/home/user/.config/net.akkudoktoreos.net/EOS.config.json",
"config_folder_path": "/home/user/.config/net.akkudoktoreos.net",
"data_cache_subpath": "cache",
"data_output_subpath": "output"
"data_output_subpath": "output",
"latitude": 52.52,
"longitude": 13.405,
"timezone": "Europe/Berlin"
}
},
"load": {
@ -324,13 +401,10 @@
}
},
"prediction": {
"$ref": "#/components/schemas/PredictionCommonSettings-Output",
"$ref": "#/components/schemas/PredictionCommonSettings",
"default": {
"historic_hours": 48,
"hours": 48,
"latitude": 52.52,
"longitude": 13.405,
"timezone": "Europe/Berlin"
"hours": 48
}
},
"pvforecast": {
@ -1989,8 +2063,8 @@
"title": "PVForecastPlaneSetting",
"type": "object"
},
"PredictionCommonSettings-Input": {
"description": "General Prediction Configuration.\n\nThis class provides configuration for prediction settings, allowing users to specify\nparameters such as the forecast duration (in hours) and location (latitude and longitude).\nValidators ensure each parameter is within a specified range. A computed property, `timezone`,\ndetermines the time zone based on latitude and longitude.\n\nAttributes:\n hours (Optional[int]): Number of hours into the future for predictions.\n Must be non-negative.\n historic_hours (Optional[int]): Number of hours into the past for historical data.\n Must be non-negative.\n latitude (Optional[float]): Latitude in degrees, must be between -90 and 90.\n longitude (Optional[float]): Longitude in degrees, must be between -180 and 180.\n\nProperties:\n timezone (Optional[str]): Computed time zone string based on the specified latitude\n and longitude.\n\nValidators:\n validate_hours (int): Ensures `hours` is a non-negative integer.\n validate_historic_hours (int): Ensures `historic_hours` is a non-negative integer.\n validate_latitude (float): Ensures `latitude` is within the range -90 to 90.\n validate_longitude (float): Ensures `longitude` is within the range -180 to 180.",
"PredictionCommonSettings": {
"description": "General Prediction Configuration.\n\nThis class provides configuration for prediction settings, allowing users to specify\nparameters such as the forecast duration (in hours).\nValidators ensure each parameter is within a specified range.\n\nAttributes:\n hours (Optional[int]): Number of hours into the future for predictions.\n Must be non-negative.\n historic_hours (Optional[int]): Number of hours into the past for historical data.\n Must be non-negative.\n\nValidators:\n validate_hours (int): Ensures `hours` is a non-negative integer.\n validate_historic_hours (int): Ensures `historic_hours` is a non-negative integer.",
"properties": {
"historic_hours": {
"anyOf": [
@ -2019,122 +2093,11 @@
"default": 48,
"description": "Number of hours into the future for predictions",
"title": "Hours"
},
"latitude": {
"anyOf": [
{
"maximum": 90.0,
"minimum": -90.0,
"type": "number"
},
{
"type": "null"
}
],
"default": 52.52,
"description": "Latitude in decimal degrees, between -90 and 90, north is positive (ISO 19115) (\u00b0)",
"title": "Latitude"
},
"longitude": {
"anyOf": [
{
"maximum": 180.0,
"minimum": -180.0,
"type": "number"
},
{
"type": "null"
}
],
"default": 13.405,
"description": "Longitude in decimal degrees, within -180 to 180 (\u00b0)",
"title": "Longitude"
}
},
"title": "PredictionCommonSettings",
"type": "object"
},
"PredictionCommonSettings-Output": {
"description": "General Prediction Configuration.\n\nThis class provides configuration for prediction settings, allowing users to specify\nparameters such as the forecast duration (in hours) and location (latitude and longitude).\nValidators ensure each parameter is within a specified range. A computed property, `timezone`,\ndetermines the time zone based on latitude and longitude.\n\nAttributes:\n hours (Optional[int]): Number of hours into the future for predictions.\n Must be non-negative.\n historic_hours (Optional[int]): Number of hours into the past for historical data.\n Must be non-negative.\n latitude (Optional[float]): Latitude in degrees, must be between -90 and 90.\n longitude (Optional[float]): Longitude in degrees, must be between -180 and 180.\n\nProperties:\n timezone (Optional[str]): Computed time zone string based on the specified latitude\n and longitude.\n\nValidators:\n validate_hours (int): Ensures `hours` is a non-negative integer.\n validate_historic_hours (int): Ensures `historic_hours` is a non-negative integer.\n validate_latitude (float): Ensures `latitude` is within the range -90 to 90.\n validate_longitude (float): Ensures `longitude` is within the range -180 to 180.",
"properties": {
"historic_hours": {
"anyOf": [
{
"minimum": 0.0,
"type": "integer"
},
{
"type": "null"
}
],
"default": 48,
"description": "Number of hours into the past for historical predictions data",
"title": "Historic Hours"
},
"hours": {
"anyOf": [
{
"minimum": 0.0,
"type": "integer"
},
{
"type": "null"
}
],
"default": 48,
"description": "Number of hours into the future for predictions",
"title": "Hours"
},
"latitude": {
"anyOf": [
{
"maximum": 90.0,
"minimum": -90.0,
"type": "number"
},
{
"type": "null"
}
],
"default": 52.52,
"description": "Latitude in decimal degrees, between -90 and 90, north is positive (ISO 19115) (\u00b0)",
"title": "Latitude"
},
"longitude": {
"anyOf": [
{
"maximum": 180.0,
"minimum": -180.0,
"type": "number"
},
{
"type": "null"
}
],
"default": 13.405,
"description": "Longitude in decimal degrees, within -180 to 180 (\u00b0)",
"title": "Longitude"
},
"timezone": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Compute timezone based on latitude and longitude.",
"readOnly": true,
"title": "Timezone"
}
},
"required": [
"timezone"
],
"title": "PredictionCommonSettings",
"type": "object"
},
"PydanticDateTimeData": {
"additionalProperties": {
"anyOf": [
@ -2404,7 +2367,7 @@
"prediction": {
"anyOf": [
{
"$ref": "#/components/schemas/PredictionCommonSettings-Input"
"$ref": "#/components/schemas/PredictionCommonSettings"
},
{
"type": "null"

View File

@ -30,11 +30,13 @@ def prepare_optimization_real_parameters() -> OptimizationParameters:
"""
# Make a config
settings = {
"general": {
"latitude": 52.52,
"longitude": 13.405,
},
"prediction": {
"hours": 48,
"historic_hours": 24,
"latitude": 52.52,
"longitude": 13.405,
},
# PV Forecast
"pvforecast": {

View File

@ -16,11 +16,13 @@ prediction_eos = get_prediction()
def config_pvforecast() -> dict:
"""Configure settings for PV forecast."""
settings = {
"general": {
"latitude": 52.52,
"longitude": 13.405,
},
"prediction": {
"hours": 48,
"historic_hours": 24,
"latitude": 52.52,
"longitude": 13.405,
},
"pvforecast": {
"provider": "PVForecastAkkudoktor",
@ -62,11 +64,13 @@ def config_pvforecast() -> dict:
def config_weather() -> dict:
"""Configure settings for weather forecast."""
settings = {
"general": {
"latitude": 52.52,
"longitude": 13.405,
},
"prediction": {
"hours": 48,
"historic_hours": 24,
"latitude": 52.52,
"longitude": 13.405,
},
"weather": dict(),
}
@ -76,11 +80,13 @@ def config_weather() -> dict:
def config_elecprice() -> dict:
"""Configure settings for electricity price forecast."""
settings = {
"general": {
"latitude": 52.52,
"longitude": 13.405,
},
"prediction": {
"hours": 48,
"historic_hours": 24,
"latitude": 52.52,
"longitude": 13.405,
},
"elecprice": dict(),
}
@ -90,12 +96,14 @@ def config_elecprice() -> dict:
def config_load() -> dict:
"""Configure settings for load forecast."""
settings = {
"general": {
"latitude": 52.52,
"longitude": 13.405,
},
"prediction": {
"hours": 48,
"historic_hours": 24,
"latitude": 52.52,
"longitude": 13.405,
}
},
}
return settings

View File

@ -39,6 +39,7 @@ from akkudoktoreos.prediction.prediction import PredictionCommonSettings
from akkudoktoreos.prediction.pvforecast import PVForecastCommonSettings
from akkudoktoreos.prediction.weather import WeatherCommonSettings
from akkudoktoreos.server.server import ServerCommonSettings
from akkudoktoreos.utils.datetimeutil import to_timezone
from akkudoktoreos.utils.utils import UtilsCommonSettings, classproperty
logger = get_logger(__name__)
@ -65,7 +66,22 @@ def get_absolute_path(
class ConfigCommonSettings(SettingsBaseModel):
"""Settings for common configuration.
General configuration to set directories of cache and output files.
General configuration to set directories of cache and output files and system location (latitude
and longitude).
Validators ensure each parameter is within a specified range. A computed property, `timezone`,
determines the time zone based on latitude and longitude.
Attributes:
latitude (Optional[float]): Latitude in degrees, must be between -90 and 90.
longitude (Optional[float]): Longitude in degrees, must be between -180 and 180.
Properties:
timezone (Optional[str]): Computed time zone string based on the specified latitude
and longitude.
Validators:
validate_latitude (float): Ensures `latitude` is within the range -90 to 90.
validate_longitude (float): Ensures `longitude` is within the range -180 to 180.
"""
_config_folder_path: ClassVar[Optional[Path]] = None
@ -83,7 +99,28 @@ class ConfigCommonSettings(SettingsBaseModel):
default="cache", description="Sub-path for the EOS cache data directory."
)
latitude: Optional[float] = Field(
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=13.405,
ge=-180.0,
le=180.0,
description="Longitude in decimal degrees, within -180 to 180 (°)",
)
# Computed fields
@computed_field # type: ignore[prop-decorator]
@property
def timezone(self) -> Optional[str]:
"""Compute timezone based on latitude and longitude."""
if self.latitude and self.longitude:
return to_timezone(location=(self.latitude, self.longitude), as_string=True)
return None
@computed_field # type: ignore[prop-decorator]
@property
def data_output_path(self) -> Optional[Path]:

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.prediction.timezone}"
url = f"{source}/prices?start={date}&end={last_date}&tz={self.config.general.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.prediction.timezone)
self.update_datetime = to_datetime(in_timezone=self.config.general.timezone)
return akkudoktor_data
def _cap_outliers(self, data: np.ndarray, sigma: int = 2) -> np.ndarray:
@ -160,7 +160,7 @@ class ElecPriceAkkudoktor(ElecPriceProvider):
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.prediction.timezone)
orig_datetime = to_datetime(value.start, in_timezone=self.config.general.timezone)
if highest_orig_datetime is None or orig_datetime > highest_orig_datetime:
highest_orig_datetime = orig_datetime

View File

@ -129,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.prediction.timezone)
self.update_datetime = to_datetime(in_timezone=self.config.general.timezone)

View File

@ -28,7 +28,7 @@ Attributes:
from typing import List, Optional, Union
from pydantic import Field, computed_field
from pydantic import Field
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.prediction.elecpriceakkudoktor import ElecPriceAkkudoktor
@ -41,34 +41,24 @@ from akkudoktoreos.prediction.pvforecastimport import PVForecastImport
from akkudoktoreos.prediction.weatherbrightsky import WeatherBrightSky
from akkudoktoreos.prediction.weatherclearoutside import WeatherClearOutside
from akkudoktoreos.prediction.weatherimport import WeatherImport
from akkudoktoreos.utils.datetimeutil import to_timezone
class PredictionCommonSettings(SettingsBaseModel):
"""General Prediction Configuration.
This class provides configuration for prediction settings, allowing users to specify
parameters such as the forecast duration (in hours) and location (latitude and longitude).
Validators ensure each parameter is within a specified range. A computed property, `timezone`,
determines the time zone based on latitude and longitude.
parameters such as the forecast duration (in hours).
Validators ensure each parameter is within a specified range.
Attributes:
hours (Optional[int]): Number of hours into the future for predictions.
Must be non-negative.
historic_hours (Optional[int]): Number of hours into the past for historical data.
Must be non-negative.
latitude (Optional[float]): Latitude in degrees, must be between -90 and 90.
longitude (Optional[float]): Longitude in degrees, must be between -180 and 180.
Properties:
timezone (Optional[str]): Computed time zone string based on the specified latitude
and longitude.
Validators:
validate_hours (int): Ensures `hours` is a non-negative integer.
validate_historic_hours (int): Ensures `historic_hours` is a non-negative integer.
validate_latitude (float): Ensures `latitude` is within the range -90 to 90.
validate_longitude (float): Ensures `longitude` is within the range -180 to 180.
"""
hours: Optional[int] = Field(
@ -79,27 +69,6 @@ class PredictionCommonSettings(SettingsBaseModel):
ge=0,
description="Number of hours into the past for historical predictions data",
)
latitude: Optional[float] = Field(
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=13.405,
ge=-180.0,
le=180.0,
description="Longitude in decimal degrees, within -180 to 180 (°)",
)
# Computed fields
@computed_field # type: ignore[prop-decorator]
@property
def timezone(self) -> Optional[str]:
"""Compute timezone based on latitude and longitude."""
if self.latitude and self.longitude:
return to_timezone(location=(self.latitude, self.longitude), as_string=True)
return None
class Prediction(PredictionContainer):

View File

@ -14,11 +14,13 @@ Classes:
Example:
# Set up the configuration with necessary fields for URL generation
settings_data = {
"general": {
"latitude": 52.52,
"longitude": 13.405,
},
"prediction": {
"hours": 48,
"historic_hours": 24,
"latitude": 52.52,
"longitude": 13.405,
},
"pvforecast": {
"provider": "PVForecastAkkudoktor",
@ -213,8 +215,8 @@ class PVForecastAkkudoktor(PVForecastProvider):
"""Build akkudoktor.net API request URL."""
base_url = "https://api.akkudoktor.net/forecast"
query_params = [
f"lat={self.config.prediction.latitude}",
f"lon={self.config.prediction.longitude}",
f"lat={self.config.general.latitude}",
f"lon={self.config.general.longitude}",
]
for i in range(len(self.config.pvforecast.planes)):
@ -236,7 +238,7 @@ class PVForecastAkkudoktor(PVForecastProvider):
"cellCoEff=-0.36",
"inverterEfficiency=0.8",
"albedo=0.25",
f"timezone={self.config.prediction.timezone}",
f"timezone={self.config.general.timezone}",
"hourly=relativehumidity_2m%2Cwindspeed_10m",
]
)
@ -265,7 +267,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.prediction.timezone)
self.update_datetime = to_datetime(in_timezone=self.config.general.timezone)
return akkudoktor_data
def _update_data(self, force_update: Optional[bool] = False) -> None:
@ -285,8 +287,8 @@ class PVForecastAkkudoktor(PVForecastProvider):
akkudoktor_data = self._request_forecast(force_update=force_update) # type: ignore
# Timezone of the PV system
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}'."
if self.config.general.timezone != akkudoktor_data.meta.timezone:
error_msg = f"Configured timezone '{self.config.general.timezone}' does not match Akkudoktor timezone '{akkudoktor_data.meta.timezone}'."
logger.error(f"Akkudoktor schema change: {error_msg}")
raise ValueError(error_msg)
@ -306,7 +308,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.prediction.timezone)
dt = to_datetime(original_datetime, in_timezone=self.config.general.timezone)
# Skip outdated forecast data
if compare_datetimes(dt, self.start_datetime.start_of("day")).lt:
@ -375,11 +377,13 @@ if __name__ == "__main__":
"""
# Set up the configuration with necessary fields for URL generation
settings_data = {
"general": {
"latitude": 52.52,
"longitude": 13.405,
},
"prediction": {
"hours": 48,
"historic_hours": 24,
"latitude": 52.52,
"longitude": 13.405,
},
"pvforecast": {
"provider": "PVForecastAkkudoktor",

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.prediction.latitude}&lon={self.config.prediction.longitude}&date={date}&last_date={last_date}&tz={self.config.prediction.timezone}"
f"{source}/weather?lat={self.config.general.latitude}&lon={self.config.general.longitude}&date={date}&last_date={last_date}&tz={self.config.general.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.prediction.timezone)
self.update_datetime = to_datetime(in_timezone=self.config.general.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.prediction.latitude, self.config.prediction.longitude, cloud_cover
self.config.general.latitude, self.config.general.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.prediction.latitude, 2)
longitude = round(self.config.prediction.longitude, 2)
latitude = round(self.config.general.latitude, 2)
longitude = round(self.config.general.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.prediction.timezone)
self.update_datetime = to_datetime(in_timezone=self.config.general.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.prediction.latitude, self.config.prediction.longitude, cloud_cover
self.config.general.latitude, self.config.general.longitude, cloud_cover
)
# Add GHI, DNI, DHI to clearout data

View File

@ -34,7 +34,7 @@ class VisualizationReport(ConfigMixin):
self.pdf_pages = PdfPages(filename, metadata={}) # Initialize PdfPages without metadata
self.version = version # overwrite version as test for constant output of pdf for test
self.current_time = to_datetime(
as_string="YYYY-MM-DD HH:mm:ss", in_timezone=self.config.prediction.timezone
as_string="YYYY-MM-DD HH:mm:ss", in_timezone=self.config.general.timezone
)
def add_chart_to_group(self, chart_func: Callable[[], None]) -> None:
@ -173,7 +173,7 @@ class VisualizationReport(ConfigMixin):
plt.grid(True)
# Add vertical line for the current date if within the axis range
current_time = pendulum.now(self.config.prediction.timezone)
current_time = pendulum.now(self.config.general.timezone)
if timestamps[0].subtract(hours=2) <= current_time <= timestamps[-1]:
plt.axvline(current_time, color="r", linestyle="--", label="Now")
plt.text(current_time, plt.ylim()[1], "Now", color="r", ha="center", va="bottom")
@ -419,9 +419,7 @@ def prepare_visualize(
start_hour: Optional[int] = 0,
) -> None:
report = VisualizationReport(filename)
next_full_hour_date = (
pendulum.now(report.config.prediction.timezone).start_of("hour").add(hours=1)
)
next_full_hour_date = pendulum.now(report.config.general.timezone).start_of("hour").add(hours=1)
# Group 1:
report.create_line_chart_date(
next_full_hour_date, # start_date

View File

@ -3,8 +3,9 @@ from pathlib import Path
from unittest.mock import patch
import pytest
from pydantic import ValidationError
from akkudoktoreos.config.config import ConfigEOS
from akkudoktoreos.config.config import ConfigCommonSettings, ConfigEOS
from akkudoktoreos.core.logging import get_logger
logger = get_logger(__name__)
@ -147,3 +148,67 @@ def test_config_copy(config_eos, monkeypatch):
assert config_eos._get_config_file_path() == (temp_config_file_path, False)
config_eos.update()
assert temp_config_file_path.exists()
@pytest.mark.parametrize(
"latitude, longitude, expected_timezone",
[
(40.7128, -74.0060, "America/New_York"), # Valid latitude/longitude
(None, None, None), # No location
(51.5074, -0.1278, "Europe/London"), # Another valid location
],
)
def test_config_common_settings_valid(latitude, longitude, expected_timezone):
"""Test valid settings for ConfigCommonSettings."""
general_settings = ConfigCommonSettings(
latitude=latitude,
longitude=longitude,
)
assert general_settings.latitude == latitude
assert general_settings.longitude == longitude
assert general_settings.timezone == expected_timezone
@pytest.mark.parametrize(
"field_name, invalid_value, expected_error",
[
("latitude", -91.0, "Input should be greater than or equal to -90"),
("latitude", 91.0, "Input should be less than or equal to 90"),
("longitude", -181.0, "Input should be greater than or equal to -180"),
("longitude", 181.0, "Input should be less than or equal to 180"),
],
)
def test_config_common_settings_invalid(field_name, invalid_value, expected_error):
"""Test invalid settings for PredictionCommonSettings."""
valid_data = {
"latitude": 40.7128,
"longitude": -74.0060,
}
assert ConfigCommonSettings(**valid_data) is not None
valid_data[field_name] = invalid_value
with pytest.raises(ValidationError, match=expected_error):
ConfigCommonSettings(**valid_data)
def test_config_common_settings_no_location():
"""Test that timezone is None when latitude and longitude are not provided."""
settings = ConfigCommonSettings(latitude=None, longitude=None)
assert settings.timezone is None
def test_config_common_settings_with_location():
"""Test that timezone is correctly computed when latitude and longitude are provided."""
settings = ConfigCommonSettings(latitude=34.0522, longitude=-118.2437)
assert settings.timezone == "America/Los_Angeles"
def test_config_common_settings_timezone_none_when_coordinates_missing():
"""Test that timezone is None when latitude or longitude is missing."""
config_no_latitude = ConfigCommonSettings(latitude=None, longitude=-74.0060)
config_no_longitude = ConfigCommonSettings(latitude=40.7128, longitude=None)
config_no_coords = ConfigCommonSettings(latitude=None, longitude=None)
assert config_no_latitude.timezone is None
assert config_no_longitude.timezone is None
assert config_no_coords.timezone is None

View File

@ -39,49 +39,18 @@ def forecast_providers():
]
@pytest.mark.parametrize(
"hours, historic_hours, latitude, longitude, expected_timezone",
[
(48, 24, 40.7128, -74.0060, "America/New_York"), # Valid latitude/longitude
(0, 0, None, None, None), # No location
(100, 50, 51.5074, -0.1278, "Europe/London"), # Another valid location
],
)
def test_prediction_common_settings_valid(
hours, historic_hours, latitude, longitude, expected_timezone
):
"""Test valid settings for PredictionCommonSettings."""
settings = PredictionCommonSettings(
hours=hours,
historic_hours=historic_hours,
latitude=latitude,
longitude=longitude,
)
assert settings.hours == hours
assert settings.historic_hours == historic_hours
assert settings.latitude == latitude
assert settings.longitude == longitude
assert settings.timezone == expected_timezone
@pytest.mark.parametrize(
"field_name, invalid_value, expected_error",
[
("hours", -1, "Input should be greater than or equal to 0"),
("historic_hours", -5, "Input should be greater than or equal to 0"),
("latitude", -91.0, "Input should be greater than or equal to -90"),
("latitude", 91.0, "Input should be less than or equal to 90"),
("longitude", -181.0, "Input should be greater than or equal to -180"),
("longitude", 181.0, "Input should be less than or equal to 180"),
],
)
def test_prediction_common_settings_invalid(field_name, invalid_value, expected_error):
def test_prediction_common_settings_invalid(field_name, invalid_value, expected_error, config_eos):
"""Test invalid settings for PredictionCommonSettings."""
valid_data = {
"hours": 48,
"historic_hours": 24,
"latitude": 40.7128,
"longitude": -74.0060,
}
assert PredictionCommonSettings(**valid_data) is not None
valid_data[field_name] = invalid_value
@ -90,31 +59,6 @@ def test_prediction_common_settings_invalid(field_name, invalid_value, expected_
PredictionCommonSettings(**valid_data)
def test_prediction_common_settings_no_location():
"""Test that timezone is None when latitude and longitude are not provided."""
settings = PredictionCommonSettings(hours=48, historic_hours=24, latitude=None, longitude=None)
assert settings.timezone is None
def test_prediction_common_settings_with_location():
"""Test that timezone is correctly computed when latitude and longitude are provided."""
settings = PredictionCommonSettings(
hours=48, historic_hours=24, latitude=34.0522, longitude=-118.2437
)
assert settings.timezone == "America/Los_Angeles"
def test_prediction_common_settings_timezone_none_when_coordinates_missing():
"""Test that timezone is None when latitude or longitude is missing."""
config_no_latitude = PredictionCommonSettings(latitude=None, longitude=-74.0060)
config_no_longitude = PredictionCommonSettings(latitude=40.7128, longitude=None)
config_no_coords = PredictionCommonSettings(latitude=None, longitude=None)
assert config_no_latitude.timezone is None
assert config_no_longitude.timezone is None
assert config_no_coords.timezone is None
def test_initialization(prediction, forecast_providers):
"""Test that Prediction is initialized with the correct providers in sequence."""
assert isinstance(prediction, Prediction)

View File

@ -88,27 +88,27 @@ class TestPredictionBase:
@pytest.fixture
def base(self, monkeypatch):
# Provide default values for configuration
monkeypatch.setenv("EOS_PREDICTION__LATITUDE", "50.0")
monkeypatch.setenv("EOS_PREDICTION__LONGITUDE", "10.0")
monkeypatch.setenv("EOS_PREDICTION__HOURS", "10")
derived = DerivedBase()
derived.config.reset_settings()
assert derived.config.prediction.hours == 10
return derived
def test_config_value_from_env_variable(self, base, monkeypatch):
# From Prediction Config
monkeypatch.setenv("EOS_PREDICTION__LATITUDE", "2.5")
monkeypatch.setenv("EOS_PREDICTION__HOURS", "2")
base.config.reset_settings()
assert base.config.prediction.latitude == 2.5
assert base.config.prediction.hours == 2
def test_config_value_from_field_default(self, base, monkeypatch):
assert base.config.prediction.model_fields["hours"].default == 48
assert base.config.prediction.hours == 48
monkeypatch.setenv("EOS_PREDICTION__HOURS", "128")
assert base.config.prediction.model_fields["historic_hours"].default == 48
assert base.config.prediction.historic_hours == 48
monkeypatch.setenv("EOS_PREDICTION__HISTORIC_HOURS", "128")
base.config.reset_settings()
assert base.config.prediction.hours == 128
monkeypatch.delenv("EOS_PREDICTION__HOURS")
assert base.config.prediction.historic_hours == 128
monkeypatch.delenv("EOS_PREDICTION__HISTORIC_HOURS")
base.config.reset_settings()
assert base.config.prediction.hours == 48
assert base.config.prediction.historic_hours == 48
def test_get_config_value_key_error(self, base):
with pytest.raises(AttributeError):
@ -185,10 +185,6 @@ class TestPredictionProvider:
# The following values are currently not set in EOS config, we can override
monkeypatch.setenv("EOS_PREDICTION__HISTORIC_HOURS", "2")
assert os.getenv("EOS_PREDICTION__HISTORIC_HOURS") == "2"
monkeypatch.setenv("EOS_PREDICTION__LATITUDE", "37.7749")
assert os.getenv("EOS_PREDICTION__LATITUDE") == "37.7749"
monkeypatch.setenv("EOS_PREDICTION__LONGITUDE", "-122.4194")
assert os.getenv("EOS_PREDICTION__LONGITUDE") == "-122.4194"
provider.config.reset_settings()
ems_eos.set_start_datetime(sample_start_datetime)
@ -196,8 +192,6 @@ class TestPredictionProvider:
assert provider.config.prediction.hours == config_eos.prediction.hours
assert provider.config.prediction.historic_hours == 2
assert provider.config.prediction.latitude == 37.7749
assert provider.config.prediction.longitude == -122.4194
assert provider.start_datetime == sample_start_datetime
assert provider.end_datetime == sample_start_datetime + to_duration(
f"{provider.config.prediction.hours} hours"

View File

@ -25,11 +25,13 @@ FILE_TESTDATA_PV_FORECAST_RESULT_1 = DIR_TESTDATA.joinpath("pv_forecast_result_1
def sample_settings(config_eos):
"""Fixture that adds settings data to the global config."""
settings = {
"general": {
"latitude": 52.52,
"longitude": 13.405,
},
"prediction": {
"hours": 48,
"historic_hours": 24,
"latitude": 52.52,
"longitude": 13.405,
},
"pvforecast": {
"provider": "PVForecastAkkudoktor",
@ -155,11 +157,13 @@ sample_value = AkkudoktorForecastValue(
windspeed_10m=10.0,
)
sample_config_data = {
"general": {
"latitude": 52.52,
"longitude": 13.405,
},
"prediction": {
"hours": 48,
"historic_hours": 24,
"latitude": 52.52,
"longitude": 13.405,
},
"pvforecast": {
"provider": "PVForecastAkkudoktor",

View File

@ -67,8 +67,8 @@ def test_invalid_provider(provider, monkeypatch):
def test_invalid_coordinates(provider, monkeypatch):
"""Test invalid coordinates raise ValueError."""
monkeypatch.setenv("EOS_PREDICTION__LATITUDE", "1000")
monkeypatch.setenv("EOS_PREDICTION__LONGITUDE", "1000")
monkeypatch.setenv("EOS_GENERAL__LATITUDE", "1000")
monkeypatch.setenv("EOS_GENERAL__LONGITUDE", "1000")
with pytest.raises(
ValueError, # match="Latitude '1000' and/ or longitude `1000` out of valid range."
):

View File

@ -27,7 +27,7 @@ def provider(config_eos):
"weather": {
"provider": "ClearOutside",
},
"prediction": {
"general": {
"latitude": 50.0,
"longitude": 10.0,
},
@ -87,7 +87,7 @@ def test_invalid_coordinates(provider, config_eos):
"weather": {
"provider": "ClearOutside",
},
"prediction": {
"general": {
"latitude": 1000.0,
"longitude": 1000.0,
},