mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2025-04-19 08:55:15 +00:00
Reasonable defaults, isolate tests, EOS_LOGGING_LEVEL, EOS_CONFIG_DIR
* Add EOS_CONFIG_DIR to set config dir (relative path to EOS_DIR or absolute path). - config_folder_path read-only - config_file_path read-only * Default values to support app start with empty config: - latitude/longitude (Berlin) - optimization_ev_available_charge_rates_percent (null, so model default value is used) - Enable Akkudoktor electricity price forecast (docker-compose). * Fix some endpoints (empty data, remove unused params, fix types). * cacheutil: Use cache dir. Closes #240 * Support EOS_LOGGING_LEVEL environment variable to set log level. * tests: All tests use separate temporary config - Add pytest switch --check-config-side-effect to check user config file existence after each test. Will also fail if user config existed before test execution (but will only check after the test has run). Enable flag in github workflow. - Globally mock platformdirs in config module. Now no longer required to patch individually. Function calls to config instance (e.g. merge_settings_from_dict) were unaffected previously. * Set Berlin as default location (default config/docker-compose).
This commit is contained in:
parent
267a9bf427
commit
75987db9e1
2
.github/workflows/pytest.yml
vendored
2
.github/workflows/pytest.yml
vendored
@ -26,7 +26,7 @@ jobs:
|
|||||||
- name: Run Pytest
|
- name: Run Pytest
|
||||||
run: |
|
run: |
|
||||||
pip install -e .
|
pip install -e .
|
||||||
python -m pytest --full-run -vs --cov src --cov-report term-missing
|
python -m pytest --full-run --check-config-side-effect -vs --cov src --cov-report term-missing
|
||||||
|
|
||||||
- name: Upload test artifacts
|
- name: Upload test artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
@ -9,6 +9,7 @@ ENV MPLCONFIGDIR="/tmp/mplconfigdir"
|
|||||||
ENV EOS_DIR="/opt/eos"
|
ENV EOS_DIR="/opt/eos"
|
||||||
ENV EOS_CACHE_DIR="${EOS_DIR}/cache"
|
ENV EOS_CACHE_DIR="${EOS_DIR}/cache"
|
||||||
ENV EOS_OUTPUT_DIR="${EOS_DIR}/output"
|
ENV EOS_OUTPUT_DIR="${EOS_DIR}/output"
|
||||||
|
ENV EOS_CONFIG_DIR="${EOS_DIR}/config"
|
||||||
|
|
||||||
WORKDIR ${EOS_DIR}
|
WORKDIR ${EOS_DIR}
|
||||||
|
|
||||||
@ -18,7 +19,9 @@ RUN adduser --system --group --no-create-home eos \
|
|||||||
&& mkdir -p "${EOS_CACHE_DIR}" \
|
&& mkdir -p "${EOS_CACHE_DIR}" \
|
||||||
&& chown eos "${EOS_CACHE_DIR}" \
|
&& chown eos "${EOS_CACHE_DIR}" \
|
||||||
&& mkdir -p "${EOS_OUTPUT_DIR}" \
|
&& mkdir -p "${EOS_OUTPUT_DIR}" \
|
||||||
&& chown eos "${EOS_OUTPUT_DIR}"
|
&& chown eos "${EOS_OUTPUT_DIR}" \
|
||||||
|
&& mkdir -p "${EOS_CONFIG_DIR}" \
|
||||||
|
&& chown eos "${EOS_CONFIG_DIR}"
|
||||||
|
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
|
|
||||||
@ -34,4 +37,4 @@ EXPOSE 8503
|
|||||||
|
|
||||||
CMD ["python", "-m", "akkudoktoreos.server.fastapi_server"]
|
CMD ["python", "-m", "akkudoktoreos.server.fastapi_server"]
|
||||||
|
|
||||||
VOLUME ["${MPLCONFIGDIR}", "${EOS_CACHE_DIR}", "${EOS_OUTPUT_DIR}"]
|
VOLUME ["${MPLCONFIGDIR}", "${EOS_CACHE_DIR}", "${EOS_OUTPUT_DIR}", "${EOS_CONFIG_DIR}"]
|
||||||
|
@ -11,7 +11,11 @@ services:
|
|||||||
dockerfile: "Dockerfile"
|
dockerfile: "Dockerfile"
|
||||||
args:
|
args:
|
||||||
PYTHON_VERSION: "${PYTHON_VERSION}"
|
PYTHON_VERSION: "${PYTHON_VERSION}"
|
||||||
volumes:
|
environment:
|
||||||
- ./src/akkudoktoreos/default.config.json:/opt/eos/EOS.config.json:ro
|
- EOS_CONFIG_DIR=config
|
||||||
|
- latitude=52.2
|
||||||
|
- longitude=13.4
|
||||||
|
- elecprice_provider=ElecPriceAkkudoktor
|
||||||
|
- elecprice_charges=0.21
|
||||||
ports:
|
ports:
|
||||||
- "${EOS_PORT}:${EOS_PORT}"
|
- "${EOS_PORT}:${EOS_PORT}"
|
||||||
|
@ -778,26 +778,8 @@
|
|||||||
},
|
},
|
||||||
"/pvforecast": {
|
"/pvforecast": {
|
||||||
"get": {
|
"get": {
|
||||||
"summary": "Fastapi Pvprognose",
|
"summary": "Fastapi Pvforecast",
|
||||||
"operationId": "fastapi_pvprognose_pvforecast_get",
|
"operationId": "fastapi_pvforecast_pvforecast_get",
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "ac_power_measurement",
|
|
||||||
"in": "query",
|
|
||||||
"required": false,
|
|
||||||
"schema": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Ac Power Measurement"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@ -808,16 +790,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Validation Error",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/HTTPValidationError"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2987,32 +2959,6 @@
|
|||||||
"description": "Sub-path for the EOS cache data directory.",
|
"description": "Sub-path for the EOS cache data directory.",
|
||||||
"default": "cache"
|
"default": "cache"
|
||||||
},
|
},
|
||||||
"config_folder_path": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"format": "path"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Config Folder Path",
|
|
||||||
"description": "Path to EOS configuration directory."
|
|
||||||
},
|
|
||||||
"config_file_path": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"format": "path"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Config File Path",
|
|
||||||
"description": "Path to EOS configuration file."
|
|
||||||
},
|
|
||||||
"pvforecast_planes": {
|
"pvforecast_planes": {
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -3100,6 +3046,34 @@
|
|||||||
"description": "Compute data_cache_path based on data_folder_path.",
|
"description": "Compute data_cache_path based on data_folder_path.",
|
||||||
"readOnly": true
|
"readOnly": true
|
||||||
},
|
},
|
||||||
|
"config_folder_path": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"format": "path"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Config Folder Path",
|
||||||
|
"description": "Path to EOS configuration directory.",
|
||||||
|
"readOnly": true
|
||||||
|
},
|
||||||
|
"config_file_path": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"format": "path"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Config File Path",
|
||||||
|
"description": "Path to EOS configuration file.",
|
||||||
|
"readOnly": true
|
||||||
|
},
|
||||||
"config_default_file_path": {
|
"config_default_file_path": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "path",
|
"format": "path",
|
||||||
@ -3128,11 +3102,13 @@
|
|||||||
"timezone",
|
"timezone",
|
||||||
"data_output_path",
|
"data_output_path",
|
||||||
"data_cache_path",
|
"data_cache_path",
|
||||||
|
"config_folder_path",
|
||||||
|
"config_file_path",
|
||||||
"config_default_file_path",
|
"config_default_file_path",
|
||||||
"config_keys"
|
"config_keys"
|
||||||
],
|
],
|
||||||
"title": "ConfigEOS",
|
"title": "ConfigEOS",
|
||||||
"description": "Singleton configuration handler for the EOS application.\n\nConfigEOS extends `SettingsEOS` with support for default configuration paths and automatic\ninitialization.\n\n`ConfigEOS` ensures that only one instance of the class is created throughout the application,\nallowing consistent access to EOS configuration settings. This singleton instance loads\nconfiguration data from a predefined set of directories or creates a default configuration if\nnone is found.\n\nInitialization Process:\n - Upon instantiation, the singleton instance attempts to load a configuration file in this order:\n 1. The directory specified by the `EOS_DIR` environment variable.\n 2. A platform specific default directory for EOS.\n 3. The current working directory.\n - The first available configuration file found in these directories is loaded.\n - If no configuration file is found, a default configuration file is created in the platform\n specific default directory, and default settings are loaded into it.\n\nAttributes from the loaded configuration are accessible directly as instance attributes of\n`ConfigEOS`, providing a centralized, shared configuration object for EOS.\n\nSingleton Behavior:\n - This class uses the `SingletonMixin` to ensure that all requests for `ConfigEOS` return\n the same instance, which contains the most up-to-date configuration. Modifying the configuration\n in one part of the application reflects across all references to this class.\n\nAttributes:\n _settings (ClassVar[SettingsEOS]): Holds application-wide settings.\n _file_settings (ClassVar[SettingsEOS]): Stores configuration loaded from file.\n config_folder_path (Optional[Path]): Path to the configuration directory.\n config_file_path (Optional[Path]): Path to the configuration file.\n\nRaises:\n FileNotFoundError: If no configuration file is found, and creating a default configuration fails.\n\nExample:\n To initialize and access configuration attributes (only one instance is created):\n ```python\n config_eos = ConfigEOS() # Always returns the same instance\n print(config_eos.prediction_hours) # Access a setting from the loaded configuration\n ```"
|
"description": "Singleton configuration handler for the EOS application.\n\nConfigEOS extends `SettingsEOS` with support for default configuration paths and automatic\ninitialization.\n\n`ConfigEOS` ensures that only one instance of the class is created throughout the application,\nallowing consistent access to EOS configuration settings. This singleton instance loads\nconfiguration data from a predefined set of directories or creates a default configuration if\nnone is found.\n\nInitialization Process:\n - Upon instantiation, the singleton instance attempts to load a configuration file in this order:\n 1. The directory specified by the `EOS_CONFIG_DIR` environment variable\n 2. The directory specified by the `EOS_DIR` environment variable.\n 3. A platform specific default directory for EOS.\n 4. The current working directory.\n - The first available configuration file found in these directories is loaded.\n - If no configuration file is found, a default configuration file is created in the platform\n specific default directory, and default settings are loaded into it.\n\nAttributes from the loaded configuration are accessible directly as instance attributes of\n`ConfigEOS`, providing a centralized, shared configuration object for EOS.\n\nSingleton Behavior:\n - This class uses the `SingletonMixin` to ensure that all requests for `ConfigEOS` return\n the same instance, which contains the most up-to-date configuration. Modifying the configuration\n in one part of the application reflects across all references to this class.\n\nAttributes:\n _settings (ClassVar[SettingsEOS]): Holds application-wide settings.\n _file_settings (ClassVar[SettingsEOS]): Stores configuration loaded from file.\n config_folder_path (Optional[Path]): Path to the configuration directory.\n config_file_path (Optional[Path]): Path to the configuration file.\n\nRaises:\n FileNotFoundError: If no configuration file is found, and creating a default configuration fails.\n\nExample:\n To initialize and access configuration attributes (only one instance is created):\n ```python\n config_eos = ConfigEOS() # Always returns the same instance\n print(config_eos.prediction_hours) # Access a setting from the loaded configuration\n ```"
|
||||||
},
|
},
|
||||||
"ElectricVehicleParameters": {
|
"ElectricVehicleParameters": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -14,7 +14,7 @@ import shutil
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, ClassVar, List, Optional
|
from typing import Any, ClassVar, List, Optional
|
||||||
|
|
||||||
import platformdirs
|
from platformdirs import user_config_dir, user_data_dir
|
||||||
from pydantic import Field, ValidationError, computed_field
|
from pydantic import Field, ValidationError, computed_field
|
||||||
|
|
||||||
# settings
|
# settings
|
||||||
@ -40,6 +40,24 @@ from akkudoktoreos.utils.utils import UtilsCommonSettings
|
|||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_absolute_path(
|
||||||
|
basepath: Optional[Path | str], subpath: Optional[Path | str]
|
||||||
|
) -> Optional[Path]:
|
||||||
|
"""Get path based on base path."""
|
||||||
|
if isinstance(basepath, str):
|
||||||
|
basepath = Path(basepath)
|
||||||
|
if subpath is None:
|
||||||
|
return basepath
|
||||||
|
|
||||||
|
if isinstance(subpath, str):
|
||||||
|
subpath = Path(subpath)
|
||||||
|
if subpath.is_absolute():
|
||||||
|
return subpath
|
||||||
|
if basepath is not None:
|
||||||
|
return basepath.joinpath(subpath)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class ConfigCommonSettings(SettingsBaseModel):
|
class ConfigCommonSettings(SettingsBaseModel):
|
||||||
"""Settings for common configuration."""
|
"""Settings for common configuration."""
|
||||||
|
|
||||||
@ -60,22 +78,14 @@ class ConfigCommonSettings(SettingsBaseModel):
|
|||||||
@property
|
@property
|
||||||
def data_output_path(self) -> Optional[Path]:
|
def data_output_path(self) -> Optional[Path]:
|
||||||
"""Compute data_output_path based on data_folder_path."""
|
"""Compute data_output_path based on data_folder_path."""
|
||||||
if self.data_output_subpath is None:
|
return get_absolute_path(self.data_folder_path, self.data_output_subpath)
|
||||||
return self.data_folder_path
|
|
||||||
if self.data_folder_path and self.data_output_subpath:
|
|
||||||
return self.data_folder_path.joinpath(self.data_output_subpath)
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Computed fields
|
# Computed fields
|
||||||
@computed_field # type: ignore[prop-decorator]
|
@computed_field # type: ignore[prop-decorator]
|
||||||
@property
|
@property
|
||||||
def data_cache_path(self) -> Optional[Path]:
|
def data_cache_path(self) -> Optional[Path]:
|
||||||
"""Compute data_cache_path based on data_folder_path."""
|
"""Compute data_cache_path based on data_folder_path."""
|
||||||
if self.data_cache_subpath is None:
|
return get_absolute_path(self.data_folder_path, self.data_cache_subpath)
|
||||||
return self.data_folder_path
|
|
||||||
if self.data_folder_path and self.data_cache_subpath:
|
|
||||||
return self.data_folder_path.joinpath(self.data_cache_subpath)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class SettingsEOS(
|
class SettingsEOS(
|
||||||
@ -114,9 +124,10 @@ class ConfigEOS(SingletonMixin, SettingsEOS):
|
|||||||
|
|
||||||
Initialization Process:
|
Initialization Process:
|
||||||
- Upon instantiation, the singleton instance attempts to load a configuration file in this order:
|
- Upon instantiation, the singleton instance attempts to load a configuration file in this order:
|
||||||
1. The directory specified by the `EOS_DIR` environment variable.
|
1. The directory specified by the `EOS_CONFIG_DIR` environment variable
|
||||||
2. A platform specific default directory for EOS.
|
2. The directory specified by the `EOS_DIR` environment variable.
|
||||||
3. The current working directory.
|
3. A platform specific default directory for EOS.
|
||||||
|
4. The current working directory.
|
||||||
- The first available configuration file found in these directories is loaded.
|
- The first available configuration file found in these directories is loaded.
|
||||||
- If no configuration file is found, a default configuration file is created in the platform
|
- If no configuration file is found, a default configuration file is created in the platform
|
||||||
specific default directory, and default settings are loaded into it.
|
specific default directory, and default settings are loaded into it.
|
||||||
@ -150,21 +161,29 @@ class ConfigEOS(SingletonMixin, SettingsEOS):
|
|||||||
APP_NAME: ClassVar[str] = "net.akkudoktor.eos" # reverse order
|
APP_NAME: ClassVar[str] = "net.akkudoktor.eos" # reverse order
|
||||||
APP_AUTHOR: ClassVar[str] = "akkudoktor"
|
APP_AUTHOR: ClassVar[str] = "akkudoktor"
|
||||||
EOS_DIR: ClassVar[str] = "EOS_DIR"
|
EOS_DIR: ClassVar[str] = "EOS_DIR"
|
||||||
|
EOS_CONFIG_DIR: ClassVar[str] = "EOS_CONFIG_DIR"
|
||||||
ENCODING: ClassVar[str] = "UTF-8"
|
ENCODING: ClassVar[str] = "UTF-8"
|
||||||
CONFIG_FILE_NAME: ClassVar[str] = "EOS.config.json"
|
CONFIG_FILE_NAME: ClassVar[str] = "EOS.config.json"
|
||||||
|
|
||||||
_settings: ClassVar[Optional[SettingsEOS]] = None
|
_settings: ClassVar[Optional[SettingsEOS]] = None
|
||||||
_file_settings: ClassVar[Optional[SettingsEOS]] = None
|
_file_settings: ClassVar[Optional[SettingsEOS]] = None
|
||||||
|
|
||||||
config_folder_path: Optional[Path] = Field(
|
_config_folder_path: Optional[Path] = None
|
||||||
None, description="Path to EOS configuration directory."
|
_config_file_path: Optional[Path] = None
|
||||||
)
|
|
||||||
|
|
||||||
config_file_path: Optional[Path] = Field(
|
|
||||||
default=None, description="Path to EOS configuration file."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Computed fields
|
# Computed fields
|
||||||
|
@computed_field # type: ignore[prop-decorator]
|
||||||
|
@property
|
||||||
|
def config_folder_path(self) -> Optional[Path]:
|
||||||
|
"""Path to EOS configuration directory."""
|
||||||
|
return self._config_folder_path
|
||||||
|
|
||||||
|
@computed_field # type: ignore[prop-decorator]
|
||||||
|
@property
|
||||||
|
def config_file_path(self) -> Optional[Path]:
|
||||||
|
"""Path to EOS configuration file."""
|
||||||
|
return self._config_file_path
|
||||||
|
|
||||||
@computed_field # type: ignore[prop-decorator]
|
@computed_field # type: ignore[prop-decorator]
|
||||||
@property
|
@property
|
||||||
def config_default_file_path(self) -> Path:
|
def config_default_file_path(self) -> Path:
|
||||||
@ -297,7 +316,7 @@ class ConfigEOS(SingletonMixin, SettingsEOS):
|
|||||||
pass
|
pass
|
||||||
# From platform specific default path
|
# From platform specific default path
|
||||||
try:
|
try:
|
||||||
data_dir = platformdirs.user_data_dir(self.APP_NAME, self.APP_AUTHOR)
|
data_dir = Path(user_data_dir(self.APP_NAME, self.APP_AUTHOR))
|
||||||
if data_dir is not None:
|
if data_dir is not None:
|
||||||
data_dir.mkdir(parents=True, exist_ok=True)
|
data_dir.mkdir(parents=True, exist_ok=True)
|
||||||
self.data_folder_path = data_dir
|
self.data_folder_path = data_dir
|
||||||
@ -308,47 +327,27 @@ class ConfigEOS(SingletonMixin, SettingsEOS):
|
|||||||
data_dir = Path.cwd()
|
data_dir = Path.cwd()
|
||||||
self.data_folder_path = data_dir
|
self.data_folder_path = data_dir
|
||||||
|
|
||||||
def _config_folder_path(self) -> Optional[Path]:
|
def _get_config_file_path(self) -> tuple[Path, bool]:
|
||||||
"""Finds the first directory containing a valid configuration file.
|
"""Finds the a valid configuration file or returns the desired path for a new config file.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Path: The path to the configuration directory, or None if not found.
|
tuple[Path, bool]: The path to the configuration directory and if there is already a config file there
|
||||||
"""
|
"""
|
||||||
config_dirs = []
|
config_dirs = []
|
||||||
config_dir = None
|
env_base_dir = os.getenv(self.EOS_DIR)
|
||||||
env_dir = os.getenv(self.EOS_DIR)
|
env_config_dir = os.getenv(self.EOS_CONFIG_DIR)
|
||||||
logger.debug(f"Envionment '{self.EOS_DIR}': '{env_dir}'")
|
env_dir = get_absolute_path(env_base_dir, env_config_dir)
|
||||||
|
logger.debug(f"Envionment config dir: '{env_dir}'")
|
||||||
if env_dir is not None:
|
if env_dir is not None:
|
||||||
config_dirs.append(Path(env_dir).resolve())
|
config_dirs.append(env_dir.resolve())
|
||||||
config_dirs.append(Path(platformdirs.user_config_dir(self.APP_NAME)))
|
config_dirs.append(Path(user_config_dir(self.APP_NAME)))
|
||||||
config_dirs.append(Path.cwd())
|
config_dirs.append(Path.cwd())
|
||||||
for cdir in config_dirs:
|
for cdir in config_dirs:
|
||||||
cfile = cdir.joinpath(self.CONFIG_FILE_NAME)
|
cfile = cdir.joinpath(self.CONFIG_FILE_NAME)
|
||||||
if cfile.exists():
|
if cfile.exists():
|
||||||
logger.debug(f"Found config file: '{cfile}'")
|
logger.debug(f"Found config file: '{cfile}'")
|
||||||
config_dir = cdir
|
return cfile, True
|
||||||
break
|
return config_dirs[0].joinpath(self.CONFIG_FILE_NAME), False
|
||||||
return config_dir
|
|
||||||
|
|
||||||
def _config_file_path(self) -> Path:
|
|
||||||
"""Finds the path to the configuration file.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Path: The path to the configuration file. May not exist.
|
|
||||||
"""
|
|
||||||
config_file = None
|
|
||||||
config_dir = self._config_folder_path()
|
|
||||||
if config_dir is None:
|
|
||||||
# There is currently no configuration file - create it in default path
|
|
||||||
env_dir = os.getenv(self.EOS_DIR)
|
|
||||||
if env_dir is not None:
|
|
||||||
config_dir = Path(env_dir).resolve()
|
|
||||||
else:
|
|
||||||
config_dir = Path(platformdirs.user_config_dir(self.APP_NAME))
|
|
||||||
config_file = config_dir.joinpath(self.CONFIG_FILE_NAME)
|
|
||||||
else:
|
|
||||||
config_file = config_dir.joinpath(self.CONFIG_FILE_NAME)
|
|
||||||
return config_file
|
|
||||||
|
|
||||||
def from_config_file(self) -> None:
|
def from_config_file(self) -> None:
|
||||||
"""Loads the configuration file settings for EOS.
|
"""Loads the configuration file settings for EOS.
|
||||||
@ -356,14 +355,14 @@ class ConfigEOS(SingletonMixin, SettingsEOS):
|
|||||||
Raises:
|
Raises:
|
||||||
ValueError: If the configuration file is invalid or incomplete.
|
ValueError: If the configuration file is invalid or incomplete.
|
||||||
"""
|
"""
|
||||||
config_file = self._config_file_path()
|
config_file, exists = self._get_config_file_path()
|
||||||
config_dir = config_file.parent
|
config_dir = config_file.parent
|
||||||
if not config_file.exists():
|
if not exists:
|
||||||
config_dir.mkdir(parents=True, exist_ok=True)
|
config_dir.mkdir(parents=True, exist_ok=True)
|
||||||
try:
|
try:
|
||||||
shutil.copy2(self.config_default_file_path, config_file)
|
shutil.copy2(self.config_default_file_path, config_file)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(f"Could not copy default config: {exc}. Using default copy...")
|
logger.warning(f"Could not copy default config: {exc}. Using default config...")
|
||||||
config_file = self.config_default_file_path
|
config_file = self.config_default_file_path
|
||||||
config_dir = config_file.parent
|
config_dir = config_file.parent
|
||||||
|
|
||||||
@ -376,8 +375,8 @@ class ConfigEOS(SingletonMixin, SettingsEOS):
|
|||||||
|
|
||||||
self.update()
|
self.update()
|
||||||
# Everthing worked, remember the values
|
# Everthing worked, remember the values
|
||||||
self.config_folder_path = config_dir
|
self._config_folder_path = config_dir
|
||||||
self.config_file_path = config_file
|
self._config_file_path = config_file
|
||||||
|
|
||||||
def to_config_file(self) -> None:
|
def to_config_file(self) -> None:
|
||||||
"""Saves the current configuration to the configuration file.
|
"""Saves the current configuration to the configuration file.
|
||||||
|
@ -6,15 +6,16 @@
|
|||||||
"data_folder_path": null,
|
"data_folder_path": null,
|
||||||
"data_output_path": null,
|
"data_output_path": null,
|
||||||
"data_output_subpath": null,
|
"data_output_subpath": null,
|
||||||
|
"elecprice_charges": 0.21,
|
||||||
"elecprice_provider": null,
|
"elecprice_provider": null,
|
||||||
"elecpriceimport_file_path": null,
|
"elecpriceimport_file_path": null,
|
||||||
"latitude": null,
|
"latitude": 52.5,
|
||||||
"load_import_file_path": null,
|
"load_import_file_path": null,
|
||||||
"load_name": null,
|
"load_name": null,
|
||||||
"load_provider": null,
|
"load_provider": null,
|
||||||
"loadakkudoktor_year_energy": null,
|
"loadakkudoktor_year_energy": null,
|
||||||
"longitude": null,
|
"longitude": 13.4,
|
||||||
"optimization_ev_available_charge_rates_percent": [],
|
"optimization_ev_available_charge_rates_percent": null,
|
||||||
"optimization_hours": 48,
|
"optimization_hours": 48,
|
||||||
"optimization_penalty": null,
|
"optimization_penalty": null,
|
||||||
"prediction_historic_hours": 48,
|
"prediction_historic_hours": 48,
|
||||||
|
@ -123,7 +123,7 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
|
|||||||
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||||
"""Decode the input array into ac_charge, dc_charge, and discharge arrays."""
|
"""Decode the input array into ac_charge, dc_charge, and discharge arrays."""
|
||||||
discharge_hours_bin_np = np.array(discharge_hours_bin)
|
discharge_hours_bin_np = np.array(discharge_hours_bin)
|
||||||
len_ac = len(self.config.optimization_ev_available_charge_rates_percent)
|
len_ac = len(self.possible_charge_values)
|
||||||
|
|
||||||
# Categorization:
|
# Categorization:
|
||||||
# Idle: 0 .. len_ac-1
|
# Idle: 0 .. len_ac-1
|
||||||
@ -155,9 +155,7 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
|
|||||||
discharge[discharge_mask] = 1 # Set Discharge states to 1
|
discharge[discharge_mask] = 1 # Set Discharge states to 1
|
||||||
|
|
||||||
ac_charge = np.zeros_like(discharge_hours_bin_np, dtype=float)
|
ac_charge = np.zeros_like(discharge_hours_bin_np, dtype=float)
|
||||||
ac_charge[ac_mask] = [
|
ac_charge[ac_mask] = [self.possible_charge_values[i] for i in ac_indices]
|
||||||
self.config.optimization_ev_available_charge_rates_percent[i] for i in ac_indices
|
|
||||||
]
|
|
||||||
|
|
||||||
# Idle is just 0, already default.
|
# Idle is just 0, already default.
|
||||||
|
|
||||||
@ -166,7 +164,7 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
|
|||||||
def mutate(self, individual: list[int]) -> tuple[list[int]]:
|
def mutate(self, individual: list[int]) -> tuple[list[int]]:
|
||||||
"""Custom mutation function for the individual."""
|
"""Custom mutation function for the individual."""
|
||||||
# Calculate the number of states
|
# Calculate the number of states
|
||||||
len_ac = len(self.config.optimization_ev_available_charge_rates_percent)
|
len_ac = len(self.possible_charge_values)
|
||||||
if self.optimize_dc_charge:
|
if self.optimize_dc_charge:
|
||||||
total_states = 3 * len_ac + 2
|
total_states = 3 * len_ac + 2
|
||||||
else:
|
else:
|
||||||
@ -300,7 +298,7 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
|
|||||||
creator.create("Individual", list, fitness=creator.FitnessMin)
|
creator.create("Individual", list, fitness=creator.FitnessMin)
|
||||||
|
|
||||||
self.toolbox = base.Toolbox()
|
self.toolbox = base.Toolbox()
|
||||||
len_ac = len(self.config.optimization_ev_available_charge_rates_percent)
|
len_ac = len(self.possible_charge_values)
|
||||||
|
|
||||||
# Total number of states without DC:
|
# Total number of states without DC:
|
||||||
# Idle: len_ac states
|
# Idle: len_ac states
|
||||||
@ -378,10 +376,7 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
|
|||||||
|
|
||||||
if eautocharge_hours_index is not None:
|
if eautocharge_hours_index is not None:
|
||||||
eautocharge_hours_float = np.array(
|
eautocharge_hours_float = np.array(
|
||||||
[
|
[self.possible_charge_values[i] for i in eautocharge_hours_index],
|
||||||
self.config.optimization_ev_available_charge_rates_percent[i]
|
|
||||||
for i in eautocharge_hours_index
|
|
||||||
],
|
|
||||||
float,
|
float,
|
||||||
)
|
)
|
||||||
self.ems.set_ev_charge_hours(eautocharge_hours_float)
|
self.ems.set_ev_charge_hours(eautocharge_hours_float)
|
||||||
@ -615,10 +610,7 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
|
|||||||
start_solution
|
start_solution
|
||||||
)
|
)
|
||||||
eautocharge_hours_float = (
|
eautocharge_hours_float = (
|
||||||
[
|
[self.possible_charge_values[i] for i in eautocharge_hours_index]
|
||||||
self.config.optimization_ev_available_charge_rates_percent[i]
|
|
||||||
for i in eautocharge_hours_index
|
|
||||||
]
|
|
||||||
if eautocharge_hours_index is not None
|
if eautocharge_hours_index is not None
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
@ -7,6 +7,7 @@ from pathlib import Path
|
|||||||
from typing import Annotated, Any, AsyncGenerator, Dict, List, Optional, Union
|
from typing import Annotated, Any, AsyncGenerator, Dict, List, Optional, Union
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
import pandas as pd
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from fastapi import FastAPI, Query, Request
|
from fastapi import FastAPI, Query, Request
|
||||||
from fastapi.exceptions import HTTPException
|
from fastapi.exceptions import HTTPException
|
||||||
@ -412,20 +413,37 @@ class ForecastResponse(PydanticBaseModel):
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/pvforecast")
|
@app.get("/pvforecast")
|
||||||
def fastapi_pvprognose(ac_power_measurement: Optional[float] = None) -> ForecastResponse:
|
def fastapi_pvforecast() -> ForecastResponse:
|
||||||
###############
|
###############
|
||||||
# PV Forecast
|
# PV Forecast
|
||||||
###############
|
###############
|
||||||
pvforecast_ac_power = prediction_eos["pvforecast_ac_power"]
|
prediction_key = "pvforecast_ac_power"
|
||||||
# Fetch prices for the specified date range
|
pvforecast_ac_power = prediction_eos.get(prediction_key)
|
||||||
pvforecast_ac_power = pvforecast_ac_power.loc[
|
if pvforecast_ac_power is None:
|
||||||
prediction_eos.start_datetime : prediction_eos.end_datetime
|
raise HTTPException(status_code=404, detail=f"Prediction not available: {prediction_key}")
|
||||||
]
|
|
||||||
pvforecastakkudoktor_temp_air = prediction_eos["pvforecastakkudoktor_temp_air"]
|
# On empty Series.loc TypeError: Cannot compare tz-naive and tz-aware datetime-like objects
|
||||||
# Fetch prices for the specified date range
|
if len(pvforecast_ac_power) == 0:
|
||||||
pvforecastakkudoktor_temp_air = pvforecastakkudoktor_temp_air.loc[
|
pvforecast_ac_power = pd.Series()
|
||||||
prediction_eos.start_datetime : prediction_eos.end_datetime
|
else:
|
||||||
]
|
# Fetch prices for the specified date range
|
||||||
|
pvforecast_ac_power = pvforecast_ac_power.loc[
|
||||||
|
prediction_eos.start_datetime : prediction_eos.end_datetime
|
||||||
|
]
|
||||||
|
|
||||||
|
prediction_key = "pvforecastakkudoktor_temp_air"
|
||||||
|
pvforecastakkudoktor_temp_air = prediction_eos.get(prediction_key)
|
||||||
|
if pvforecastakkudoktor_temp_air is None:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Prediction not available: {prediction_key}")
|
||||||
|
|
||||||
|
# On empty Series.loc TypeError: Cannot compare tz-naive and tz-aware datetime-like objects
|
||||||
|
if len(pvforecastakkudoktor_temp_air) == 0:
|
||||||
|
pvforecastakkudoktor_temp_air = pd.Series()
|
||||||
|
else:
|
||||||
|
# Fetch prices for the specified date range
|
||||||
|
pvforecastakkudoktor_temp_air = pvforecastakkudoktor_temp_air.loc[
|
||||||
|
prediction_eos.start_datetime : prediction_eos.end_datetime
|
||||||
|
]
|
||||||
|
|
||||||
# Return both forecasts as a JSON response
|
# Return both forecasts as a JSON response
|
||||||
return ForecastResponse(
|
return ForecastResponse(
|
||||||
@ -457,8 +475,8 @@ def fastapi_optimize(
|
|||||||
def get_pdf() -> PdfResponse:
|
def get_pdf() -> PdfResponse:
|
||||||
# Endpoint to serve the generated PDF with visualization results
|
# Endpoint to serve the generated PDF with visualization results
|
||||||
output_path = config_eos.data_output_path
|
output_path = config_eos.data_output_path
|
||||||
if not output_path.is_dir():
|
if output_path is None or not output_path.is_dir():
|
||||||
raise ValueError(f"Output path does not exist: {output_path}.")
|
raise HTTPException(status_code=404, detail=f"Output path does not exist: {output_path}.")
|
||||||
file_path = output_path / "visualization_results.pdf"
|
file_path = output_path / "visualization_results.pdf"
|
||||||
if not file_path.is_file():
|
if not file_path.is_file():
|
||||||
raise HTTPException(status_code=404, detail="No visualization result available.")
|
raise HTTPException(status_code=404, detail="No visualization result available.")
|
||||||
|
@ -47,6 +47,7 @@ from typing import (
|
|||||||
from pendulum import DateTime, Duration
|
from pendulum import DateTime, Duration
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
from akkudoktoreos.core.coreabc import ConfigMixin
|
||||||
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime, to_duration
|
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime, to_duration
|
||||||
from akkudoktoreos.utils.logutil import get_logger
|
from akkudoktoreos.utils.logutil import get_logger
|
||||||
|
|
||||||
@ -90,7 +91,7 @@ class CacheFileStoreMeta(type, Generic[T]):
|
|||||||
return cls._instances[cls]
|
return cls._instances[cls]
|
||||||
|
|
||||||
|
|
||||||
class CacheFileStore(metaclass=CacheFileStoreMeta):
|
class CacheFileStore(ConfigMixin, metaclass=CacheFileStoreMeta):
|
||||||
"""A key-value store that manages file-like tempfile objects to be used as cache files.
|
"""A key-value store that manages file-like tempfile objects to be used as cache files.
|
||||||
|
|
||||||
Cache files are associated with a date. If no date is specified, the cache files are
|
Cache files are associated with a date. If no date is specified, the cache files are
|
||||||
@ -328,8 +329,9 @@ class CacheFileStore(metaclass=CacheFileStoreMeta):
|
|||||||
# File already available
|
# File already available
|
||||||
cache_file_obj = cache_item.cache_file
|
cache_file_obj = cache_item.cache_file
|
||||||
else:
|
else:
|
||||||
|
self.config.data_cache_path.mkdir(parents=True, exist_ok=True)
|
||||||
cache_file_obj = tempfile.NamedTemporaryFile(
|
cache_file_obj = tempfile.NamedTemporaryFile(
|
||||||
mode=mode, delete=delete, suffix=suffix
|
mode=mode, delete=delete, suffix=suffix, dir=self.config.data_cache_path
|
||||||
)
|
)
|
||||||
self._store[cache_file_key] = CacheFileRecord(
|
self._store[cache_file_key] = CacheFileRecord(
|
||||||
cache_file=cache_file_obj,
|
cache_file=cache_file_obj,
|
||||||
|
@ -50,6 +50,8 @@ def get_logger(
|
|||||||
# Create a logger with the specified name
|
# Create a logger with the specified name
|
||||||
logger = logging.getLogger(name)
|
logger = logging.getLogger(name)
|
||||||
logger.propagate = True
|
logger.propagate = True
|
||||||
|
if (env_level := os.getenv("EOS_LOGGING_LEVEL")) is not None:
|
||||||
|
logging_level = env_level
|
||||||
if logging_level == "DEBUG":
|
if logging_level == "DEBUG":
|
||||||
level = logging.DEBUG
|
level = logging.DEBUG
|
||||||
elif logging_level == "INFO":
|
elif logging_level == "INFO":
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
import logging
|
import logging
|
||||||
import shutil
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from unittest.mock import PropertyMock, patch
|
||||||
|
|
||||||
import pendulum
|
import pendulum
|
||||||
import platformdirs
|
|
||||||
import pytest
|
import pytest
|
||||||
from xprocess import ProcessStarter
|
from xprocess import ProcessStarter
|
||||||
|
|
||||||
from akkudoktoreos.config.config import get_config
|
from akkudoktoreos.config.config import ConfigEOS, get_config
|
||||||
from akkudoktoreos.utils.logutil import get_logger
|
from akkudoktoreos.utils.logutil import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
@ -42,6 +42,12 @@ def pytest_addoption(parser):
|
|||||||
parser.addoption(
|
parser.addoption(
|
||||||
"--full-run", action="store_true", default=False, help="Run with all optimization tests."
|
"--full-run", action="store_true", default=False, help="Run with all optimization tests."
|
||||||
)
|
)
|
||||||
|
parser.addoption(
|
||||||
|
"--check-config-side-effect",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Verify that user config file is non-existent (will also fail if user config file exists before test run).",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -49,97 +55,107 @@ def is_full_run(request):
|
|||||||
yield bool(request.config.getoption("--full-run"))
|
yield bool(request.config.getoption("--full-run"))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def config_mixin(config_eos):
|
||||||
|
with patch(
|
||||||
|
"akkudoktoreos.core.coreabc.ConfigMixin.config", new_callable=PropertyMock
|
||||||
|
) as config_mixin_patch:
|
||||||
|
config_mixin_patch.return_value = config_eos
|
||||||
|
yield config_mixin_patch
|
||||||
|
|
||||||
|
|
||||||
|
# Test if test has side effect of writing to system (user) config file
|
||||||
|
# Before activating, make sure that no user config file exists (e.g. ~/.config/net.akkudoktoreos.eos/EOS.config.json)
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def cfg_non_existent(request):
|
||||||
|
yield
|
||||||
|
if bool(request.config.getoption("--check-config-side-effect")):
|
||||||
|
from platformdirs import user_config_dir
|
||||||
|
|
||||||
|
user_dir = user_config_dir(ConfigEOS.APP_NAME)
|
||||||
|
assert not Path(user_dir).joinpath(ConfigEOS.CONFIG_FILE_NAME).exists()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def user_config_dir(config_default_dirs):
|
||||||
|
with patch(
|
||||||
|
"akkudoktoreos.config.config.user_config_dir",
|
||||||
|
return_value=str(config_default_dirs[0]),
|
||||||
|
) as user_dir_patch:
|
||||||
|
yield user_dir_patch
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def user_data_dir(config_default_dirs):
|
||||||
|
with patch(
|
||||||
|
"akkudoktoreos.config.config.user_data_dir",
|
||||||
|
return_value=str(config_default_dirs[-1] / "data"),
|
||||||
|
) as user_dir_patch:
|
||||||
|
yield user_dir_patch
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def reset_config(disable_debug_logging):
|
def config_eos(
|
||||||
|
disable_debug_logging,
|
||||||
|
user_config_dir,
|
||||||
|
user_data_dir,
|
||||||
|
config_default_dirs,
|
||||||
|
monkeypatch,
|
||||||
|
) -> ConfigEOS:
|
||||||
"""Fixture to reset EOS config to default values."""
|
"""Fixture to reset EOS config to default values."""
|
||||||
|
monkeypatch.setenv("data_cache_subpath", str(config_default_dirs[-1] / "data/cache"))
|
||||||
|
monkeypatch.setenv("data_output_subpath", str(config_default_dirs[-1] / "data/output"))
|
||||||
|
config_file = config_default_dirs[0] / ConfigEOS.CONFIG_FILE_NAME
|
||||||
|
assert not config_file.exists()
|
||||||
config_eos = get_config()
|
config_eos = get_config()
|
||||||
config_eos.reset_settings()
|
config_eos.reset_settings()
|
||||||
config_eos.reset_to_defaults()
|
assert config_file == config_eos.config_file_path
|
||||||
|
assert config_file.exists()
|
||||||
|
assert config_default_dirs[-1] / "data" == config_eos.data_folder_path
|
||||||
|
assert config_default_dirs[-1] / "data/cache" == config_eos.data_cache_path
|
||||||
|
assert config_default_dirs[-1] / "data/output" == config_eos.data_output_path
|
||||||
return config_eos
|
return config_eos
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def config_default_dirs():
|
def config_default_dirs():
|
||||||
"""Fixture that provides a list of directories to be used as config dir."""
|
"""Fixture that provides a list of directories to be used as config dir."""
|
||||||
config_eos = get_config()
|
with tempfile.TemporaryDirectory() as tmp_user_home_dir:
|
||||||
# Default config directory from platform user config directory
|
# Default config directory from platform user config directory
|
||||||
config_default_dir_user = Path(platformdirs.user_config_dir(config_eos.APP_NAME))
|
config_default_dir_user = Path(tmp_user_home_dir) / "config"
|
||||||
# Default config directory from current working directory
|
# Default config directory from current working directory
|
||||||
config_default_dir_cwd = Path.cwd()
|
config_default_dir_cwd = Path.cwd()
|
||||||
# Default config directory from default config file
|
# Default config directory from default config file
|
||||||
config_default_dir_default = Path(__file__).parent.parent.joinpath("src/akkudoktoreos/data")
|
config_default_dir_default = Path(__file__).parent.parent.joinpath("src/akkudoktoreos/data")
|
||||||
return config_default_dir_user, config_default_dir_cwd, config_default_dir_default
|
|
||||||
|
# Default data directory from platform user data directory
|
||||||
|
data_default_dir_user = Path(tmp_user_home_dir)
|
||||||
|
yield (
|
||||||
|
config_default_dir_user,
|
||||||
|
config_default_dir_cwd,
|
||||||
|
config_default_dir_default,
|
||||||
|
data_default_dir_user,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def stash_config_file(config_default_dirs):
|
def server(xprocess, config_eos, config_default_dirs):
|
||||||
"""Fixture to temporarily stash away an existing config file during a test.
|
|
||||||
|
|
||||||
If the specified config file exists, it moves the file to a temporary directory.
|
|
||||||
The file is restored to its original location after the test.
|
|
||||||
|
|
||||||
Keep right most in fixture parameter list to assure application at last.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Path: Path to the stashed config file.
|
|
||||||
"""
|
|
||||||
config_eos = get_config()
|
|
||||||
config_default_dir_user, config_default_dir_cwd, _ = config_default_dirs
|
|
||||||
|
|
||||||
config_file_path_user = config_default_dir_user.joinpath(config_eos.CONFIG_FILE_NAME)
|
|
||||||
config_file_path_cwd = config_default_dir_cwd.joinpath(config_eos.CONFIG_FILE_NAME)
|
|
||||||
|
|
||||||
original_config_file_user = None
|
|
||||||
original_config_file_cwd = None
|
|
||||||
if config_file_path_user.exists():
|
|
||||||
original_config_file_user = config_file_path_user
|
|
||||||
if config_file_path_cwd.exists():
|
|
||||||
original_config_file_cwd = config_file_path_cwd
|
|
||||||
|
|
||||||
temp_dir = tempfile.TemporaryDirectory()
|
|
||||||
temp_file_user = None
|
|
||||||
temp_file_cwd = None
|
|
||||||
|
|
||||||
# If the file exists, move it to the temporary directory
|
|
||||||
if original_config_file_user:
|
|
||||||
temp_file_user = Path(temp_dir.name) / f"user.{original_config_file_user.name}"
|
|
||||||
shutil.move(original_config_file_user, temp_file_user)
|
|
||||||
assert not original_config_file_user.exists()
|
|
||||||
logger.debug(f"Stashed: '{original_config_file_user}'")
|
|
||||||
if original_config_file_cwd:
|
|
||||||
temp_file_cwd = Path(temp_dir.name) / f"cwd.{original_config_file_cwd.name}"
|
|
||||||
shutil.move(original_config_file_cwd, temp_file_cwd)
|
|
||||||
assert not original_config_file_cwd.exists()
|
|
||||||
logger.debug(f"Stashed: '{original_config_file_cwd}'")
|
|
||||||
|
|
||||||
# Yield the temporary file path to the test
|
|
||||||
yield temp_file_user, temp_file_cwd
|
|
||||||
|
|
||||||
# Cleanup after the test
|
|
||||||
if temp_file_user:
|
|
||||||
# Restore the file to its original location
|
|
||||||
shutil.move(temp_file_user, original_config_file_user)
|
|
||||||
assert original_config_file_user.exists()
|
|
||||||
if temp_file_cwd:
|
|
||||||
# Restore the file to its original location
|
|
||||||
shutil.move(temp_file_cwd, original_config_file_cwd)
|
|
||||||
assert original_config_file_cwd.exists()
|
|
||||||
temp_dir.cleanup()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def server(xprocess, tmp_path: Path):
|
|
||||||
"""Fixture to start the server.
|
"""Fixture to start the server.
|
||||||
|
|
||||||
Provides URL of the server.
|
Provides URL of the server.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Starter(ProcessStarter):
|
class Starter(ProcessStarter):
|
||||||
|
# Set environment before any subprocess run, to keep custom config dir
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["EOS_DIR"] = str(config_default_dirs[-1])
|
||||||
|
|
||||||
# assure server to be installed
|
# assure server to be installed
|
||||||
try:
|
try:
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
[sys.executable, "-c", "import akkudoktoreos.server.fastapi_server"],
|
[sys.executable, "-c", "import akkudoktoreos.server.fastapi_server"],
|
||||||
check=True,
|
check=True,
|
||||||
|
env=env,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
)
|
)
|
||||||
@ -154,11 +170,6 @@ def server(xprocess, tmp_path: Path):
|
|||||||
|
|
||||||
# command to start server process
|
# command to start server process
|
||||||
args = [sys.executable, "-m", "akkudoktoreos.server.fastapi_server"]
|
args = [sys.executable, "-m", "akkudoktoreos.server.fastapi_server"]
|
||||||
config_eos = get_config()
|
|
||||||
settings = {
|
|
||||||
"data_folder_path": tmp_path,
|
|
||||||
}
|
|
||||||
config_eos.merge_settings_from_dict(settings)
|
|
||||||
|
|
||||||
# startup pattern
|
# startup pattern
|
||||||
pattern = "Application startup complete."
|
pattern = "Application startup complete."
|
||||||
@ -174,6 +185,7 @@ def server(xprocess, tmp_path: Path):
|
|||||||
|
|
||||||
# ensure process is running and return its logfile
|
# ensure process is running and return its logfile
|
||||||
pid, logfile = xprocess.ensure("eos", Starter)
|
pid, logfile = xprocess.ensure("eos", Starter)
|
||||||
|
print(f"View xprocess logfile at: {logfile}")
|
||||||
|
|
||||||
# create url/port info to the server
|
# create url/port info to the server
|
||||||
url = "http://127.0.0.1:8503"
|
url = "http://127.0.0.1:8503"
|
||||||
|
@ -3,7 +3,6 @@ from pathlib import Path
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from akkudoktoreos.config.config import get_config
|
|
||||||
from akkudoktoreos.core.ems import (
|
from akkudoktoreos.core.ems import (
|
||||||
EnergieManagementSystem,
|
EnergieManagementSystem,
|
||||||
EnergieManagementSystemParameters,
|
EnergieManagementSystemParameters,
|
||||||
@ -24,10 +23,9 @@ start_hour = 1
|
|||||||
|
|
||||||
# Example initialization of necessary components
|
# Example initialization of necessary components
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def create_ems_instance() -> EnergieManagementSystem:
|
def create_ems_instance(config_eos) -> EnergieManagementSystem:
|
||||||
"""Fixture to create an EnergieManagementSystem instance with given test parameters."""
|
"""Fixture to create an EnergieManagementSystem instance with given test parameters."""
|
||||||
# Assure configuration holds the correct values
|
# Assure configuration holds the correct values
|
||||||
config_eos = get_config()
|
|
||||||
config_eos.merge_settings_from_dict({"prediction_hours": 48, "optimization_hours": 24})
|
config_eos.merge_settings_from_dict({"prediction_hours": 48, "optimization_hours": 24})
|
||||||
assert config_eos.prediction_hours is not None
|
assert config_eos.prediction_hours is not None
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ from pathlib import Path
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from akkudoktoreos.config.config import get_config
|
|
||||||
from akkudoktoreos.core.ems import (
|
from akkudoktoreos.core.ems import (
|
||||||
EnergieManagementSystem,
|
EnergieManagementSystem,
|
||||||
EnergieManagementSystemParameters,
|
EnergieManagementSystemParameters,
|
||||||
@ -23,10 +22,9 @@ start_hour = 0
|
|||||||
|
|
||||||
# Example initialization of necessary components
|
# Example initialization of necessary components
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def create_ems_instance() -> EnergieManagementSystem:
|
def create_ems_instance(config_eos) -> EnergieManagementSystem:
|
||||||
"""Fixture to create an EnergieManagementSystem instance with given test parameters."""
|
"""Fixture to create an EnergieManagementSystem instance with given test parameters."""
|
||||||
# Assure configuration holds the correct values
|
# Assure configuration holds the correct values
|
||||||
config_eos = get_config()
|
|
||||||
config_eos.merge_settings_from_dict({"prediction_hours": 48, "optimization_hours": 24})
|
config_eos.merge_settings_from_dict({"prediction_hours": 48, "optimization_hours": 24})
|
||||||
assert config_eos.prediction_hours is not None
|
assert config_eos.prediction_hours is not None
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ from unittest.mock import patch
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from akkudoktoreos.config.config import get_config
|
from akkudoktoreos.config.config import ConfigEOS
|
||||||
from akkudoktoreos.optimization.genetic import (
|
from akkudoktoreos.optimization.genetic import (
|
||||||
OptimizationParameters,
|
OptimizationParameters,
|
||||||
OptimizeResponse,
|
OptimizeResponse,
|
||||||
@ -40,10 +40,15 @@ def compare_dict(actual: dict[str, Any], expected: dict[str, Any]):
|
|||||||
("optimize_input_2.json", "optimize_result_2_full.json", 400),
|
("optimize_input_2.json", "optimize_result_2_full.json", 400),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_optimize(fn_in: str, fn_out: str, ngen: int, is_full_run: bool):
|
def test_optimize(
|
||||||
|
fn_in: str,
|
||||||
|
fn_out: str,
|
||||||
|
ngen: int,
|
||||||
|
config_eos: ConfigEOS,
|
||||||
|
is_full_run: bool,
|
||||||
|
):
|
||||||
"""Test optimierung_ems."""
|
"""Test optimierung_ems."""
|
||||||
# Assure configuration holds the correct values
|
# Assure configuration holds the correct values
|
||||||
config_eos = get_config()
|
|
||||||
config_eos.merge_settings_from_dict({"prediction_hours": 48, "optimization_hours": 48})
|
config_eos.merge_settings_from_dict({"prediction_hours": 48, "optimization_hours": 48})
|
||||||
|
|
||||||
# Load input and output data
|
# Load input and output data
|
||||||
@ -93,3 +98,4 @@ def test_optimize(fn_in: str, fn_out: str, ngen: int, is_full_run: bool):
|
|||||||
|
|
||||||
# The function creates a visualization result PDF as a side-effect.
|
# The function creates a visualization result PDF as a side-effect.
|
||||||
prepare_visualize_patch.assert_called_once()
|
prepare_visualize_patch.assert_called_once()
|
||||||
|
assert Path(visualize_filename).exists()
|
||||||
|
@ -1,43 +1,33 @@
|
|||||||
import os
|
|
||||||
import shutil
|
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from akkudoktoreos.config.config import ConfigEOS, get_config
|
from akkudoktoreos.config.config import ConfigEOS
|
||||||
from akkudoktoreos.utils.logutil import get_logger
|
from akkudoktoreos.utils.logutil import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
config_eos = get_config()
|
|
||||||
|
|
||||||
DIR_TESTDATA = Path(__file__).absolute().parent.joinpath("testdata")
|
# overwrite config_mixin fixture from conftest
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
FILE_TESTDATA_CONFIGEOS_1_JSON = DIR_TESTDATA.joinpath(config_eos.CONFIG_FILE_NAME)
|
def config_mixin():
|
||||||
FILE_TESTDATA_CONFIGEOS_1_DIR = FILE_TESTDATA_CONFIGEOS_1_JSON.parent
|
pass
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
def test_fixture_new_config_file(config_default_dirs):
|
||||||
def reset_config_singleton():
|
|
||||||
"""Fixture to reset the ConfigEOS singleton instance before a test."""
|
|
||||||
ConfigEOS.reset_instance()
|
|
||||||
yield
|
|
||||||
ConfigEOS.reset_instance()
|
|
||||||
|
|
||||||
|
|
||||||
def test_fixture_stash_config_file(stash_config_file, config_default_dirs):
|
|
||||||
"""Assure fixture stash_config_file is working."""
|
"""Assure fixture stash_config_file is working."""
|
||||||
config_default_dir_user, config_default_dir_cwd, _ = config_default_dirs
|
config_default_dir_user, config_default_dir_cwd, _, _ = config_default_dirs
|
||||||
|
|
||||||
config_file_path_user = config_default_dir_user.joinpath(config_eos.CONFIG_FILE_NAME)
|
config_file_path_user = config_default_dir_user.joinpath(ConfigEOS.CONFIG_FILE_NAME)
|
||||||
config_file_path_cwd = config_default_dir_cwd.joinpath(config_eos.CONFIG_FILE_NAME)
|
config_file_path_cwd = config_default_dir_cwd.joinpath(ConfigEOS.CONFIG_FILE_NAME)
|
||||||
|
|
||||||
assert not config_file_path_user.exists()
|
assert not config_file_path_user.exists()
|
||||||
assert not config_file_path_cwd.exists()
|
assert not config_file_path_cwd.exists()
|
||||||
|
|
||||||
|
|
||||||
def test_config_constants():
|
def test_config_constants(config_eos):
|
||||||
"""Test config constants are the way expected by the tests."""
|
"""Test config constants are the way expected by the tests."""
|
||||||
assert config_eos.APP_NAME == "net.akkudoktor.eos"
|
assert config_eos.APP_NAME == "net.akkudoktor.eos"
|
||||||
assert config_eos.APP_AUTHOR == "akkudoktor"
|
assert config_eos.APP_AUTHOR == "akkudoktor"
|
||||||
@ -46,7 +36,7 @@ def test_config_constants():
|
|||||||
assert config_eos.CONFIG_FILE_NAME == "EOS.config.json"
|
assert config_eos.CONFIG_FILE_NAME == "EOS.config.json"
|
||||||
|
|
||||||
|
|
||||||
def test_computed_paths(reset_config):
|
def test_computed_paths(config_eos):
|
||||||
"""Test computed paths for output and cache."""
|
"""Test computed paths for output and cache."""
|
||||||
config_eos.merge_settings_from_dict(
|
config_eos.merge_settings_from_dict(
|
||||||
{
|
{
|
||||||
@ -57,56 +47,81 @@ def test_computed_paths(reset_config):
|
|||||||
)
|
)
|
||||||
assert config_eos.data_output_path == Path("/base/data/output")
|
assert config_eos.data_output_path == Path("/base/data/output")
|
||||||
assert config_eos.data_cache_path == Path("/base/data/cache")
|
assert config_eos.data_cache_path == Path("/base/data/cache")
|
||||||
|
# reset settings so the config_eos fixture can verify the default paths
|
||||||
|
config_eos.reset_settings()
|
||||||
|
|
||||||
|
|
||||||
def test_singleton_behavior(reset_config_singleton):
|
def test_singleton_behavior(config_eos, config_default_dirs):
|
||||||
"""Test that ConfigEOS behaves as a singleton."""
|
"""Test that ConfigEOS behaves as a singleton."""
|
||||||
instance1 = ConfigEOS()
|
initial_cfg_file = config_eos.config_file_path
|
||||||
instance2 = ConfigEOS()
|
with patch(
|
||||||
|
"akkudoktoreos.config.config.user_config_dir", return_value=str(config_default_dirs[0])
|
||||||
|
):
|
||||||
|
instance1 = ConfigEOS()
|
||||||
|
instance2 = ConfigEOS()
|
||||||
|
assert instance1 is config_eos
|
||||||
assert instance1 is instance2
|
assert instance1 is instance2
|
||||||
|
assert instance1.config_file_path == initial_cfg_file
|
||||||
|
|
||||||
|
|
||||||
def test_default_config_path(reset_config, config_default_dirs, stash_config_file):
|
def test_default_config_path(config_eos, config_default_dirs):
|
||||||
"""Test that the default config file path is computed correctly."""
|
"""Test that the default config file path is computed correctly."""
|
||||||
_, _, config_default_dir_default = config_default_dirs
|
_, _, config_default_dir_default, _ = config_default_dirs
|
||||||
|
|
||||||
expected_path = config_default_dir_default.joinpath("default.config.json")
|
expected_path = config_default_dir_default.joinpath("default.config.json")
|
||||||
assert config_eos.config_default_file_path == expected_path
|
assert config_eos.config_default_file_path == expected_path
|
||||||
assert config_eos.config_default_file_path.is_file()
|
assert config_eos.config_default_file_path.is_file()
|
||||||
|
|
||||||
|
|
||||||
def test_config_folder_path(reset_config, config_default_dirs, stash_config_file, monkeypatch):
|
@patch("akkudoktoreos.config.config.user_config_dir")
|
||||||
"""Test that _config_folder_path identifies the correct config directory or None."""
|
def test_get_config_file_path(user_config_dir_patch, config_eos, config_default_dirs, monkeypatch):
|
||||||
config_default_dir_user, _, _ = config_default_dirs
|
"""Test that _get_config_file_path identifies the correct config file."""
|
||||||
|
config_default_dir_user, _, _, _ = config_default_dirs
|
||||||
|
user_config_dir_patch.return_value = str(config_default_dir_user)
|
||||||
|
|
||||||
# All config files are stashed away, no config folder path
|
def cfg_file(dir: Path) -> Path:
|
||||||
assert config_eos._config_folder_path() is None
|
return dir.joinpath(ConfigEOS.CONFIG_FILE_NAME)
|
||||||
|
|
||||||
config_file_user = config_default_dir_user.joinpath(config_eos.CONFIG_FILE_NAME)
|
# Config newly created from fixture with fresh user config directory
|
||||||
shutil.copy2(config_eos.config_default_file_path, config_file_user)
|
assert config_eos._get_config_file_path() == (cfg_file(config_default_dir_user), True)
|
||||||
assert config_eos._config_folder_path() == config_default_dir_user
|
cfg_file(config_default_dir_user).unlink()
|
||||||
|
|
||||||
monkeypatch.setenv("EOS_DIR", str(FILE_TESTDATA_CONFIGEOS_1_DIR))
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
assert config_eos._config_folder_path() == FILE_TESTDATA_CONFIGEOS_1_DIR
|
temp_dir_path = Path(temp_dir)
|
||||||
|
monkeypatch.setenv("EOS_DIR", str(temp_dir_path))
|
||||||
|
assert config_eos._get_config_file_path() == (cfg_file(temp_dir_path), False)
|
||||||
|
|
||||||
# Cleanup after the test
|
monkeypatch.setenv("EOS_CONFIG_DIR", "config")
|
||||||
os.remove(config_file_user)
|
assert config_eos._get_config_file_path() == (
|
||||||
|
cfg_file(temp_dir_path / "config"),
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setenv("EOS_CONFIG_DIR", str(temp_dir_path / "config2"))
|
||||||
|
assert config_eos._get_config_file_path() == (
|
||||||
|
cfg_file(temp_dir_path / "config2"),
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.delenv("EOS_DIR")
|
||||||
|
monkeypatch.setenv("EOS_CONFIG_DIR", "config3")
|
||||||
|
assert config_eos._get_config_file_path() == (cfg_file(config_default_dir_user), False)
|
||||||
|
|
||||||
|
monkeypatch.setenv("EOS_CONFIG_DIR", str(temp_dir_path / "config3"))
|
||||||
|
assert config_eos._get_config_file_path() == (
|
||||||
|
cfg_file(temp_dir_path / "config3"),
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_config_copy(reset_config, stash_config_file, monkeypatch):
|
def test_config_copy(config_eos, monkeypatch):
|
||||||
"""Test if the config is copied to the provided path."""
|
"""Test if the config is copied to the provided path."""
|
||||||
temp_dir = tempfile.TemporaryDirectory()
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
temp_folder_path = Path(temp_dir.name)
|
temp_folder_path = Path(temp_dir)
|
||||||
temp_config_file_path = temp_folder_path.joinpath(config_eos.CONFIG_FILE_NAME).resolve()
|
temp_config_file_path = temp_folder_path.joinpath(config_eos.CONFIG_FILE_NAME).resolve()
|
||||||
monkeypatch.setenv(config_eos.EOS_DIR, str(temp_folder_path))
|
monkeypatch.setenv(config_eos.EOS_DIR, str(temp_folder_path))
|
||||||
if temp_config_file_path.exists():
|
assert not temp_config_file_path.exists()
|
||||||
temp_config_file_path.unlink()
|
with patch("akkudoktoreos.config.config.user_config_dir", return_value=temp_dir):
|
||||||
assert not temp_config_file_path.exists()
|
assert config_eos._get_config_file_path() == (temp_config_file_path, False)
|
||||||
assert config_eos._config_folder_path() is None
|
config_eos.from_config_file()
|
||||||
assert config_eos._config_file_path() == temp_config_file_path
|
assert temp_config_file_path.exists()
|
||||||
|
|
||||||
config_eos.from_config_file()
|
|
||||||
assert temp_config_file_path.exists()
|
|
||||||
|
|
||||||
# Cleanup after the test
|
|
||||||
temp_dir.cleanup()
|
|
||||||
|
@ -113,7 +113,7 @@ class DerivedDataContainer(DataContainer):
|
|||||||
|
|
||||||
class TestDataBase:
|
class TestDataBase:
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def base(self, reset_config, monkeypatch):
|
def base(self):
|
||||||
# Provide default values for configuration
|
# Provide default values for configuration
|
||||||
derived = DerivedBase()
|
derived = DerivedBase()
|
||||||
derived.config.update()
|
derived.config.update()
|
||||||
|
@ -21,8 +21,6 @@ FILE_TESTDATA_ELECPRICEAKKUDOKTOR_1_JSON = DIR_TESTDATA.joinpath(
|
|||||||
"elecpriceforecast_akkudoktor_1.json"
|
"elecpriceforecast_akkudoktor_1.json"
|
||||||
)
|
)
|
||||||
|
|
||||||
ems_eos = get_ems()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def elecprice_provider(monkeypatch):
|
def elecprice_provider(monkeypatch):
|
||||||
@ -136,6 +134,7 @@ def test_update_data(mock_get, elecprice_provider, sample_akkudoktor_1_json, cac
|
|||||||
cache_store.clear(clear_all=True)
|
cache_store.clear(clear_all=True)
|
||||||
|
|
||||||
# Call the method
|
# Call the method
|
||||||
|
ems_eos = get_ems()
|
||||||
ems_eos.set_start_datetime(to_datetime("2024-12-11 00:00:00", in_timezone="Europe/Berlin"))
|
ems_eos.set_start_datetime(to_datetime("2024-12-11 00:00:00", in_timezone="Europe/Berlin"))
|
||||||
elecprice_provider.update_data(force_enable=True, force_update=True)
|
elecprice_provider.update_data(force_enable=True, force_update=True)
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from akkudoktoreos.config.config import get_config
|
|
||||||
from akkudoktoreos.core.ems import get_ems
|
from akkudoktoreos.core.ems import get_ems
|
||||||
from akkudoktoreos.prediction.elecpriceimport import ElecPriceImport
|
from akkudoktoreos.prediction.elecpriceimport import ElecPriceImport
|
||||||
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime
|
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime
|
||||||
@ -12,12 +11,9 @@ DIR_TESTDATA = Path(__file__).absolute().parent.joinpath("testdata")
|
|||||||
|
|
||||||
FILE_TESTDATA_ELECPRICEIMPORT_1_JSON = DIR_TESTDATA.joinpath("import_input_1.json")
|
FILE_TESTDATA_ELECPRICEIMPORT_1_JSON = DIR_TESTDATA.joinpath("import_input_1.json")
|
||||||
|
|
||||||
config_eos = get_config()
|
|
||||||
ems_eos = get_ems()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def elecprice_provider(reset_config, sample_import_1_json):
|
def elecprice_provider(sample_import_1_json, config_eos):
|
||||||
"""Fixture to create a ElecPriceProvider instance."""
|
"""Fixture to create a ElecPriceProvider instance."""
|
||||||
settings = {
|
settings = {
|
||||||
"elecprice_provider": "ElecPriceImport",
|
"elecprice_provider": "ElecPriceImport",
|
||||||
@ -26,7 +22,7 @@ def elecprice_provider(reset_config, sample_import_1_json):
|
|||||||
}
|
}
|
||||||
config_eos.merge_settings_from_dict(settings)
|
config_eos.merge_settings_from_dict(settings)
|
||||||
provider = ElecPriceImport()
|
provider = ElecPriceImport()
|
||||||
assert provider.enabled() == True
|
assert provider.enabled()
|
||||||
return provider
|
return provider
|
||||||
|
|
||||||
|
|
||||||
@ -49,14 +45,14 @@ def test_singleton_instance(elecprice_provider):
|
|||||||
assert elecprice_provider is another_instance
|
assert elecprice_provider is another_instance
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_provider(elecprice_provider):
|
def test_invalid_provider(elecprice_provider, config_eos):
|
||||||
"""Test requesting an unsupported elecprice_provider."""
|
"""Test requesting an unsupported elecprice_provider."""
|
||||||
settings = {
|
settings = {
|
||||||
"elecprice_provider": "<invalid>",
|
"elecprice_provider": "<invalid>",
|
||||||
"elecpriceimport_file_path": str(FILE_TESTDATA_ELECPRICEIMPORT_1_JSON),
|
"elecpriceimport_file_path": str(FILE_TESTDATA_ELECPRICEIMPORT_1_JSON),
|
||||||
}
|
}
|
||||||
config_eos.merge_settings_from_dict(settings)
|
config_eos.merge_settings_from_dict(settings)
|
||||||
assert elecprice_provider.enabled() == False
|
assert not elecprice_provider.enabled()
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------
|
# ------------------------------------------------
|
||||||
@ -77,8 +73,9 @@ def test_invalid_provider(elecprice_provider):
|
|||||||
("2024-10-27 00:00:00", False), # DST change in Germany (25 hours/ day)
|
("2024-10-27 00:00:00", False), # DST change in Germany (25 hours/ day)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_import(elecprice_provider, sample_import_1_json, start_datetime, from_file):
|
def test_import(elecprice_provider, sample_import_1_json, start_datetime, from_file, config_eos):
|
||||||
"""Test fetching forecast from Import."""
|
"""Test fetching forecast from Import."""
|
||||||
|
ems_eos = get_ems()
|
||||||
ems_eos.set_start_datetime(to_datetime(start_datetime, in_timezone="Europe/Berlin"))
|
ems_eos.set_start_datetime(to_datetime(start_datetime, in_timezone="Europe/Berlin"))
|
||||||
if from_file:
|
if from_file:
|
||||||
config_eos.elecpriceimport_json = None
|
config_eos.elecpriceimport_json = None
|
||||||
|
@ -4,7 +4,6 @@ import numpy as np
|
|||||||
import pendulum
|
import pendulum
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from akkudoktoreos.config.config import get_config
|
|
||||||
from akkudoktoreos.core.ems import get_ems
|
from akkudoktoreos.core.ems import get_ems
|
||||||
from akkudoktoreos.measurement.measurement import MeasurementDataRecord, get_measurement
|
from akkudoktoreos.measurement.measurement import MeasurementDataRecord, get_measurement
|
||||||
from akkudoktoreos.prediction.loadakkudoktor import (
|
from akkudoktoreos.prediction.loadakkudoktor import (
|
||||||
@ -13,12 +12,9 @@ from akkudoktoreos.prediction.loadakkudoktor import (
|
|||||||
)
|
)
|
||||||
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime, to_duration
|
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime, to_duration
|
||||||
|
|
||||||
config_eos = get_config()
|
|
||||||
ems_eos = get_ems()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def load_provider():
|
def load_provider(config_eos):
|
||||||
"""Fixture to initialise the LoadAkkudoktor instance."""
|
"""Fixture to initialise the LoadAkkudoktor instance."""
|
||||||
settings = {
|
settings = {
|
||||||
"load_provider": "LoadAkkudoktor",
|
"load_provider": "LoadAkkudoktor",
|
||||||
@ -112,6 +108,7 @@ def test_update_data(mock_load_data, load_provider):
|
|||||||
mock_load_data.return_value = np.random.rand(365, 2, 24)
|
mock_load_data.return_value = np.random.rand(365, 2, 24)
|
||||||
|
|
||||||
# Mock methods for updating values
|
# Mock methods for updating values
|
||||||
|
ems_eos = get_ems()
|
||||||
ems_eos.set_start_datetime(pendulum.datetime(2024, 1, 1))
|
ems_eos.set_start_datetime(pendulum.datetime(2024, 1, 1))
|
||||||
|
|
||||||
# Assure there are no prediction records
|
# Assure there are no prediction records
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
from generate_openapi import generate_openapi
|
|
||||||
|
|
||||||
DIR_PROJECT_ROOT = Path(__file__).parent.parent
|
DIR_PROJECT_ROOT = Path(__file__).parent.parent
|
||||||
DIR_TESTDATA = Path(__file__).parent / "testdata"
|
DIR_TESTDATA = Path(__file__).parent / "testdata"
|
||||||
|
|
||||||
|
|
||||||
def test_openapi_spec_current():
|
def test_openapi_spec_current(config_eos):
|
||||||
"""Verify the openapi spec hasn´t changed."""
|
"""Verify the openapi spec hasn´t changed."""
|
||||||
old_spec_path = DIR_PROJECT_ROOT / "docs" / "akkudoktoreos" / "openapi.json"
|
old_spec_path = DIR_PROJECT_ROOT / "docs" / "akkudoktoreos" / "openapi.json"
|
||||||
new_spec_path = DIR_TESTDATA / "openapi-new.json"
|
new_spec_path = DIR_TESTDATA / "openapi-new.json"
|
||||||
generate_openapi(new_spec_path)
|
# Patch get_config and import within guard to patch global variables within the fastapi_server module.
|
||||||
|
with patch("akkudoktoreos.config.config.get_config", return_value=config_eos):
|
||||||
|
from generate_openapi import generate_openapi
|
||||||
|
|
||||||
|
generate_openapi(new_spec_path)
|
||||||
with open(new_spec_path) as f_new:
|
with open(new_spec_path) as f_new:
|
||||||
new_spec = json.load(f_new)
|
new_spec = json.load(f_new)
|
||||||
with open(old_spec_path) as f_old:
|
with open(old_spec_path) as f_old:
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from akkudoktoreos.config.config import get_config
|
|
||||||
from akkudoktoreos.prediction.elecpriceakkudoktor import ElecPriceAkkudoktor
|
from akkudoktoreos.prediction.elecpriceakkudoktor import ElecPriceAkkudoktor
|
||||||
from akkudoktoreos.prediction.elecpriceimport import ElecPriceImport
|
from akkudoktoreos.prediction.elecpriceimport import ElecPriceImport
|
||||||
from akkudoktoreos.prediction.loadakkudoktor import LoadAkkudoktor
|
from akkudoktoreos.prediction.loadakkudoktor import LoadAkkudoktor
|
||||||
@ -19,7 +18,7 @@ from akkudoktoreos.prediction.weatherimport import WeatherImport
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def sample_settings(reset_config):
|
def sample_settings(config_eos):
|
||||||
"""Fixture that adds settings data to the global config."""
|
"""Fixture that adds settings data to the global config."""
|
||||||
settings = {
|
settings = {
|
||||||
"prediction_hours": 48,
|
"prediction_hours": 48,
|
||||||
@ -33,9 +32,8 @@ def sample_settings(reset_config):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Merge settings to config
|
# Merge settings to config
|
||||||
config = get_config()
|
config_eos.merge_settings_from_dict(settings)
|
||||||
config.merge_settings_from_dict(settings)
|
return config_eos
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
@ -7,7 +7,6 @@ import pendulum
|
|||||||
import pytest
|
import pytest
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|
||||||
from akkudoktoreos.config.config import get_config
|
|
||||||
from akkudoktoreos.core.ems import get_ems
|
from akkudoktoreos.core.ems import get_ems
|
||||||
from akkudoktoreos.prediction.prediction import PredictionCommonSettings
|
from akkudoktoreos.prediction.prediction import PredictionCommonSettings
|
||||||
from akkudoktoreos.prediction.predictionabc import (
|
from akkudoktoreos.prediction.predictionabc import (
|
||||||
@ -87,7 +86,7 @@ class DerivedPredictionContainer(PredictionContainer):
|
|||||||
|
|
||||||
class TestPredictionBase:
|
class TestPredictionBase:
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def base(self, reset_config, monkeypatch):
|
def base(self, monkeypatch):
|
||||||
# Provide default values for configuration
|
# Provide default values for configuration
|
||||||
monkeypatch.setenv("latitude", "50.0")
|
monkeypatch.setenv("latitude", "50.0")
|
||||||
monkeypatch.setenv("longitude", "10.0")
|
monkeypatch.setenv("longitude", "10.0")
|
||||||
@ -177,10 +176,11 @@ class TestPredictionProvider:
|
|||||||
provider.keep_datetime == expected_keep_datetime
|
provider.keep_datetime == expected_keep_datetime
|
||||||
), "Keep datetime is not calculated correctly."
|
), "Keep datetime is not calculated correctly."
|
||||||
|
|
||||||
def test_update_method_with_defaults(self, provider, sample_start_datetime, monkeypatch):
|
def test_update_method_with_defaults(
|
||||||
|
self, provider, sample_start_datetime, config_eos, monkeypatch
|
||||||
|
):
|
||||||
"""Test the `update` method with default parameters."""
|
"""Test the `update` method with default parameters."""
|
||||||
# EOS config supersedes
|
# EOS config supersedes
|
||||||
config_eos = get_config()
|
|
||||||
ems_eos = get_ems()
|
ems_eos = get_ems()
|
||||||
# The following values are currently not set in EOS config, we can override
|
# The following values are currently not set in EOS config, we can override
|
||||||
monkeypatch.setenv("prediction_historic_hours", "2")
|
monkeypatch.setenv("prediction_historic_hours", "2")
|
||||||
|
@ -4,7 +4,6 @@ from unittest.mock import Mock, patch
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from akkudoktoreos.config.config import get_config
|
|
||||||
from akkudoktoreos.core.ems import get_ems
|
from akkudoktoreos.core.ems import get_ems
|
||||||
from akkudoktoreos.prediction.prediction import get_prediction
|
from akkudoktoreos.prediction.prediction import get_prediction
|
||||||
from akkudoktoreos.prediction.pvforecastakkudoktor import (
|
from akkudoktoreos.prediction.pvforecastakkudoktor import (
|
||||||
@ -22,12 +21,8 @@ FILE_TESTDATA_PV_FORECAST_INPUT_1 = DIR_TESTDATA.joinpath("pv_forecast_input_1.j
|
|||||||
FILE_TESTDATA_PV_FORECAST_RESULT_1 = DIR_TESTDATA.joinpath("pv_forecast_result_1.txt")
|
FILE_TESTDATA_PV_FORECAST_RESULT_1 = DIR_TESTDATA.joinpath("pv_forecast_result_1.txt")
|
||||||
|
|
||||||
|
|
||||||
config_eos = get_config()
|
|
||||||
ems_eos = get_ems()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def sample_settings(reset_config):
|
def sample_settings(config_eos):
|
||||||
"""Fixture that adds settings data to the global config."""
|
"""Fixture that adds settings data to the global config."""
|
||||||
settings = {
|
settings = {
|
||||||
"prediction_hours": 48,
|
"prediction_hours": 48,
|
||||||
@ -223,6 +218,7 @@ def test_pvforecast_akkudoktor_update_with_sample_forecast(
|
|||||||
mock_get.return_value = mock_response
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
# Test that update properly inserts data records
|
# Test that update properly inserts data records
|
||||||
|
ems_eos = get_ems()
|
||||||
ems_eos.set_start_datetime(sample_forecast_start)
|
ems_eos.set_start_datetime(sample_forecast_start)
|
||||||
provider.update_data(force_enable=True, force_update=True)
|
provider.update_data(force_enable=True, force_update=True)
|
||||||
assert compare_datetimes(provider.start_datetime, sample_forecast_start).equal
|
assert compare_datetimes(provider.start_datetime, sample_forecast_start).equal
|
||||||
@ -230,10 +226,9 @@ def test_pvforecast_akkudoktor_update_with_sample_forecast(
|
|||||||
|
|
||||||
|
|
||||||
# Report Generation Test
|
# Report Generation Test
|
||||||
def test_report_ac_power_and_measurement(provider):
|
def test_report_ac_power_and_measurement(provider, config_eos):
|
||||||
# Set the configuration
|
# Set the configuration
|
||||||
config = get_config()
|
config_eos.merge_settings_from_dict(sample_config_data)
|
||||||
config.merge_settings_from_dict(sample_config_data)
|
|
||||||
|
|
||||||
record = PVForecastAkkudoktorDataRecord(
|
record = PVForecastAkkudoktorDataRecord(
|
||||||
pvforecastakkudoktor_ac_power_measured=900.0,
|
pvforecastakkudoktor_ac_power_measured=900.0,
|
||||||
@ -275,6 +270,7 @@ def test_timezone_behaviour(
|
|||||||
|
|
||||||
provider.clear()
|
provider.clear()
|
||||||
assert len(provider) == 0
|
assert len(provider) == 0
|
||||||
|
ems_eos = get_ems()
|
||||||
ems_eos.set_start_datetime(other_start_datetime)
|
ems_eos.set_start_datetime(other_start_datetime)
|
||||||
provider.update_data(force_update=True)
|
provider.update_data(force_update=True)
|
||||||
assert compare_datetimes(provider.start_datetime, other_start_datetime).equal
|
assert compare_datetimes(provider.start_datetime, other_start_datetime).equal
|
||||||
|
@ -3,7 +3,6 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from akkudoktoreos.config.config import get_config
|
|
||||||
from akkudoktoreos.core.ems import get_ems
|
from akkudoktoreos.core.ems import get_ems
|
||||||
from akkudoktoreos.prediction.pvforecastimport import PVForecastImport
|
from akkudoktoreos.prediction.pvforecastimport import PVForecastImport
|
||||||
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime
|
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime
|
||||||
@ -12,12 +11,9 @@ DIR_TESTDATA = Path(__file__).absolute().parent.joinpath("testdata")
|
|||||||
|
|
||||||
FILE_TESTDATA_PVFORECASTIMPORT_1_JSON = DIR_TESTDATA.joinpath("import_input_1.json")
|
FILE_TESTDATA_PVFORECASTIMPORT_1_JSON = DIR_TESTDATA.joinpath("import_input_1.json")
|
||||||
|
|
||||||
config_eos = get_config()
|
|
||||||
ems_eos = get_ems()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def pvforecast_provider(reset_config, sample_import_1_json):
|
def pvforecast_provider(sample_import_1_json, config_eos):
|
||||||
"""Fixture to create a PVForecastProvider instance."""
|
"""Fixture to create a PVForecastProvider instance."""
|
||||||
settings = {
|
settings = {
|
||||||
"pvforecast_provider": "PVForecastImport",
|
"pvforecast_provider": "PVForecastImport",
|
||||||
@ -26,7 +22,7 @@ def pvforecast_provider(reset_config, sample_import_1_json):
|
|||||||
}
|
}
|
||||||
config_eos.merge_settings_from_dict(settings)
|
config_eos.merge_settings_from_dict(settings)
|
||||||
provider = PVForecastImport()
|
provider = PVForecastImport()
|
||||||
assert provider.enabled() == True
|
assert provider.enabled()
|
||||||
return provider
|
return provider
|
||||||
|
|
||||||
|
|
||||||
@ -49,14 +45,14 @@ def test_singleton_instance(pvforecast_provider):
|
|||||||
assert pvforecast_provider is another_instance
|
assert pvforecast_provider is another_instance
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_provider(pvforecast_provider):
|
def test_invalid_provider(pvforecast_provider, config_eos):
|
||||||
"""Test requesting an unsupported pvforecast_provider."""
|
"""Test requesting an unsupported pvforecast_provider."""
|
||||||
settings = {
|
settings = {
|
||||||
"pvforecast_provider": "<invalid>",
|
"pvforecast_provider": "<invalid>",
|
||||||
"pvforecastimport_file_path": str(FILE_TESTDATA_PVFORECASTIMPORT_1_JSON),
|
"pvforecastimport_file_path": str(FILE_TESTDATA_PVFORECASTIMPORT_1_JSON),
|
||||||
}
|
}
|
||||||
config_eos.merge_settings_from_dict(settings)
|
config_eos.merge_settings_from_dict(settings)
|
||||||
assert pvforecast_provider.enabled() == False
|
assert not pvforecast_provider.enabled()
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------
|
# ------------------------------------------------
|
||||||
@ -77,8 +73,9 @@ def test_invalid_provider(pvforecast_provider):
|
|||||||
("2024-10-27 00:00:00", False), # DST change in Germany (25 hours/ day)
|
("2024-10-27 00:00:00", False), # DST change in Germany (25 hours/ day)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_import(pvforecast_provider, sample_import_1_json, start_datetime, from_file):
|
def test_import(pvforecast_provider, sample_import_1_json, start_datetime, from_file, config_eos):
|
||||||
"""Test fetching forecast from import."""
|
"""Test fetching forecast from import."""
|
||||||
|
ems_eos = get_ems()
|
||||||
ems_eos.set_start_datetime(to_datetime(start_datetime, in_timezone="Europe/Berlin"))
|
ems_eos.set_start_datetime(to_datetime(start_datetime, in_timezone="Europe/Berlin"))
|
||||||
if from_file:
|
if from_file:
|
||||||
config_eos.pvforecastimport_json = None
|
config_eos.pvforecastimport_json = None
|
||||||
|
@ -2,12 +2,8 @@ from http import HTTPStatus
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from akkudoktoreos.config.config import get_config
|
|
||||||
|
|
||||||
config_eos = get_config()
|
def test_server(server, config_eos):
|
||||||
|
|
||||||
|
|
||||||
def test_server(server):
|
|
||||||
"""Test the server."""
|
"""Test the server."""
|
||||||
# validate correct path in server
|
# validate correct path in server
|
||||||
assert config_eos.data_folder_path is not None
|
assert config_eos.data_folder_path is not None
|
||||||
|
@ -1,32 +1,28 @@
|
|||||||
import os
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from matplotlib.testing.compare import compare_images
|
from matplotlib.testing.compare import compare_images
|
||||||
|
|
||||||
from akkudoktoreos.config.config import get_config
|
|
||||||
from akkudoktoreos.utils.visualize import generate_example_report
|
from akkudoktoreos.utils.visualize import generate_example_report
|
||||||
|
|
||||||
filename = "example_report.pdf"
|
filename = "example_report.pdf"
|
||||||
|
|
||||||
config = get_config()
|
|
||||||
output_dir = config.data_output_path
|
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
output_file = os.path.join(output_dir, filename)
|
|
||||||
|
|
||||||
DIR_TESTDATA = Path(__file__).parent / "testdata"
|
DIR_TESTDATA = Path(__file__).parent / "testdata"
|
||||||
reference_file = DIR_TESTDATA / "test_example_report.pdf"
|
reference_file = DIR_TESTDATA / "test_example_report.pdf"
|
||||||
|
|
||||||
|
|
||||||
def test_generate_pdf_example():
|
def test_generate_pdf_example(config_eos):
|
||||||
"""Test generation of example visualization report."""
|
"""Test generation of example visualization report."""
|
||||||
# Delete the old generated file if it exists
|
output_dir = config_eos.data_output_path
|
||||||
if os.path.isfile(output_file):
|
assert output_dir is not None
|
||||||
os.remove(output_file)
|
output_file = output_dir / filename
|
||||||
|
assert not output_file.exists()
|
||||||
|
|
||||||
generate_example_report(filename)
|
# Generate PDF
|
||||||
|
generate_example_report()
|
||||||
|
|
||||||
# Check if the file exists
|
# Check if the file exists
|
||||||
assert os.path.isfile(output_file)
|
assert output_file.exists()
|
||||||
|
|
||||||
# Compare the generated file with the reference file
|
# Compare the generated file with the reference file
|
||||||
comparison = compare_images(str(reference_file), str(output_file), tol=0)
|
comparison = compare_images(str(reference_file), str(output_file), tol=0)
|
||||||
|
@ -15,8 +15,6 @@ DIR_TESTDATA = Path(__file__).absolute().parent.joinpath("testdata")
|
|||||||
FILE_TESTDATA_WEATHERBRIGHTSKY_1_JSON = DIR_TESTDATA.joinpath("weatherforecast_brightsky_1.json")
|
FILE_TESTDATA_WEATHERBRIGHTSKY_1_JSON = DIR_TESTDATA.joinpath("weatherforecast_brightsky_1.json")
|
||||||
FILE_TESTDATA_WEATHERBRIGHTSKY_2_JSON = DIR_TESTDATA.joinpath("weatherforecast_brightsky_2.json")
|
FILE_TESTDATA_WEATHERBRIGHTSKY_2_JSON = DIR_TESTDATA.joinpath("weatherforecast_brightsky_2.json")
|
||||||
|
|
||||||
ems_eos = get_ems()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def weather_provider(monkeypatch):
|
def weather_provider(monkeypatch):
|
||||||
@ -64,7 +62,7 @@ def test_invalid_provider(weather_provider, monkeypatch):
|
|||||||
"""Test requesting an unsupported weather_provider."""
|
"""Test requesting an unsupported weather_provider."""
|
||||||
monkeypatch.setenv("weather_provider", "<invalid>")
|
monkeypatch.setenv("weather_provider", "<invalid>")
|
||||||
weather_provider.config.update()
|
weather_provider.config.update()
|
||||||
assert weather_provider.enabled() == False
|
assert not weather_provider.enabled()
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_coordinates(weather_provider, monkeypatch):
|
def test_invalid_coordinates(weather_provider, monkeypatch):
|
||||||
@ -163,6 +161,7 @@ def test_update_data(mock_get, weather_provider, sample_brightsky_1_json, cache_
|
|||||||
cache_store.clear(clear_all=True)
|
cache_store.clear(clear_all=True)
|
||||||
|
|
||||||
# Call the method
|
# Call the method
|
||||||
|
ems_eos = get_ems()
|
||||||
ems_eos.set_start_datetime(to_datetime("2024-10-26 00:00:00", in_timezone="Europe/Berlin"))
|
ems_eos.set_start_datetime(to_datetime("2024-10-26 00:00:00", in_timezone="Europe/Berlin"))
|
||||||
weather_provider.update_data(force_enable=True, force_update=True)
|
weather_provider.update_data(force_enable=True, force_update=True)
|
||||||
|
|
||||||
|
@ -9,7 +9,6 @@ import pvlib
|
|||||||
import pytest
|
import pytest
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
from akkudoktoreos.config.config import get_config
|
|
||||||
from akkudoktoreos.core.ems import get_ems
|
from akkudoktoreos.core.ems import get_ems
|
||||||
from akkudoktoreos.prediction.weatherclearoutside import WeatherClearOutside
|
from akkudoktoreos.prediction.weatherclearoutside import WeatherClearOutside
|
||||||
from akkudoktoreos.utils.cacheutil import CacheFileStore
|
from akkudoktoreos.utils.cacheutil import CacheFileStore
|
||||||
@ -20,12 +19,9 @@ DIR_TESTDATA = Path(__file__).absolute().parent.joinpath("testdata")
|
|||||||
FILE_TESTDATA_WEATHERCLEAROUTSIDE_1_HTML = DIR_TESTDATA.joinpath("weatherforecast_clearout_1.html")
|
FILE_TESTDATA_WEATHERCLEAROUTSIDE_1_HTML = DIR_TESTDATA.joinpath("weatherforecast_clearout_1.html")
|
||||||
FILE_TESTDATA_WEATHERCLEAROUTSIDE_1_DATA = DIR_TESTDATA.joinpath("weatherforecast_clearout_1.json")
|
FILE_TESTDATA_WEATHERCLEAROUTSIDE_1_DATA = DIR_TESTDATA.joinpath("weatherforecast_clearout_1.json")
|
||||||
|
|
||||||
config_eos = get_config()
|
|
||||||
ems_eos = get_ems()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def weather_provider():
|
def weather_provider(config_eos):
|
||||||
"""Fixture to create a WeatherProvider instance."""
|
"""Fixture to create a WeatherProvider instance."""
|
||||||
settings = {
|
settings = {
|
||||||
"weather_provider": "ClearOutside",
|
"weather_provider": "ClearOutside",
|
||||||
@ -70,16 +66,16 @@ def test_singleton_instance(weather_provider):
|
|||||||
assert weather_provider is another_instance
|
assert weather_provider is another_instance
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_provider(weather_provider):
|
def test_invalid_provider(weather_provider, config_eos):
|
||||||
"""Test requesting an unsupported weather_provider."""
|
"""Test requesting an unsupported weather_provider."""
|
||||||
settings = {
|
settings = {
|
||||||
"weather_provider": "<invalid>",
|
"weather_provider": "<invalid>",
|
||||||
}
|
}
|
||||||
config_eos.merge_settings_from_dict(settings)
|
config_eos.merge_settings_from_dict(settings)
|
||||||
assert weather_provider.enabled() == False
|
assert not weather_provider.enabled()
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_coordinates(weather_provider):
|
def test_invalid_coordinates(weather_provider, config_eos):
|
||||||
"""Test invalid coordinates raise ValueError."""
|
"""Test invalid coordinates raise ValueError."""
|
||||||
settings = {
|
settings = {
|
||||||
"weather_provider": "ClearOutside",
|
"weather_provider": "ClearOutside",
|
||||||
@ -118,7 +114,7 @@ def test_irridiance_estimate_from_cloud_cover(weather_provider):
|
|||||||
|
|
||||||
|
|
||||||
@patch("requests.get")
|
@patch("requests.get")
|
||||||
def test_request_forecast(mock_get, weather_provider, sample_clearout_1_html):
|
def test_request_forecast(mock_get, weather_provider, sample_clearout_1_html, config_eos):
|
||||||
"""Test fetching forecast from ClearOutside."""
|
"""Test fetching forecast from ClearOutside."""
|
||||||
# Mock response object
|
# Mock response object
|
||||||
mock_response = Mock()
|
mock_response = Mock()
|
||||||
@ -149,6 +145,7 @@ def test_update_data(mock_get, weather_provider, sample_clearout_1_html, sample_
|
|||||||
expected_keep = to_datetime("2024-10-24 00:00:00", in_timezone="Europe/Berlin")
|
expected_keep = to_datetime("2024-10-24 00:00:00", in_timezone="Europe/Berlin")
|
||||||
|
|
||||||
# Call the method
|
# Call the method
|
||||||
|
ems_eos = get_ems()
|
||||||
ems_eos.set_start_datetime(expected_start)
|
ems_eos.set_start_datetime(expected_start)
|
||||||
weather_provider.update_data()
|
weather_provider.update_data()
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from akkudoktoreos.config.config import get_config
|
|
||||||
from akkudoktoreos.core.ems import get_ems
|
from akkudoktoreos.core.ems import get_ems
|
||||||
from akkudoktoreos.prediction.weatherimport import WeatherImport
|
from akkudoktoreos.prediction.weatherimport import WeatherImport
|
||||||
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime
|
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime
|
||||||
@ -12,12 +11,9 @@ DIR_TESTDATA = Path(__file__).absolute().parent.joinpath("testdata")
|
|||||||
|
|
||||||
FILE_TESTDATA_WEATHERIMPORT_1_JSON = DIR_TESTDATA.joinpath("import_input_1.json")
|
FILE_TESTDATA_WEATHERIMPORT_1_JSON = DIR_TESTDATA.joinpath("import_input_1.json")
|
||||||
|
|
||||||
config_eos = get_config()
|
|
||||||
ems_eos = get_ems()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def weather_provider(reset_config, sample_import_1_json):
|
def weather_provider(sample_import_1_json, config_eos):
|
||||||
"""Fixture to create a WeatherProvider instance."""
|
"""Fixture to create a WeatherProvider instance."""
|
||||||
settings = {
|
settings = {
|
||||||
"weather_provider": "WeatherImport",
|
"weather_provider": "WeatherImport",
|
||||||
@ -49,7 +45,7 @@ def test_singleton_instance(weather_provider):
|
|||||||
assert weather_provider is another_instance
|
assert weather_provider is another_instance
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_provider(weather_provider, monkeypatch):
|
def test_invalid_provider(weather_provider, config_eos, monkeypatch):
|
||||||
"""Test requesting an unsupported weather_provider."""
|
"""Test requesting an unsupported weather_provider."""
|
||||||
settings = {
|
settings = {
|
||||||
"weather_provider": "<invalid>",
|
"weather_provider": "<invalid>",
|
||||||
@ -77,8 +73,9 @@ def test_invalid_provider(weather_provider, monkeypatch):
|
|||||||
("2024-10-27 00:00:00", False), # DST change in Germany (25 hours/ day)
|
("2024-10-27 00:00:00", False), # DST change in Germany (25 hours/ day)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_import(weather_provider, sample_import_1_json, start_datetime, from_file):
|
def test_import(weather_provider, sample_import_1_json, start_datetime, from_file, config_eos):
|
||||||
"""Test fetching forecast from Import."""
|
"""Test fetching forecast from Import."""
|
||||||
|
ems_eos = get_ems()
|
||||||
ems_eos.set_start_datetime(to_datetime(start_datetime, in_timezone="Europe/Berlin"))
|
ems_eos.set_start_datetime(to_datetime(start_datetime, in_timezone="Europe/Berlin"))
|
||||||
if from_file:
|
if from_file:
|
||||||
config_eos.weatherimport_json = None
|
config_eos.weatherimport_json = None
|
||||||
|
Loading…
x
Reference in New Issue
Block a user