mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-03-09 08:06:17 +00:00
fix: prevent exception when load prediction data is missing (#925)
Some checks failed
Bump Version / Bump Version Workflow (push) Has been cancelled
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Some checks failed
Bump Version / Bump Version Workflow (push) Has been cancelled
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Validate solution prediction data before processing. If required prediction data is missing, the prediction is skipped instead of raising an exception. Introduce a new configuration file saving policy to improve loading robustness: - Exclude computed fields - Exclude fields set to their default values - Exclude fields with value None - Use field aliases - Recursively remove empty dictionaries and lists - Ensure general.version is always present and correctly set When loading older configuration files, computed fields are now stripped before migration. This further improves backward compatibility and loading robustness. Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
# the root directory (no add-on folder as usual).
|
# the root directory (no add-on folder as usual).
|
||||||
|
|
||||||
name: "Akkudoktor-EOS"
|
name: "Akkudoktor-EOS"
|
||||||
version: "0.2.0.dev2602281697121815"
|
version: "0.2.0.dev2603032000228213"
|
||||||
slug: "eos"
|
slug: "eos"
|
||||||
description: "Akkudoktor-EOS add-on"
|
description: "Akkudoktor-EOS add-on"
|
||||||
url: "https://github.com/Akkudoktor-EOS/EOS"
|
url: "https://github.com/Akkudoktor-EOS/EOS"
|
||||||
|
|||||||
@@ -120,7 +120,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"version": "0.2.0.dev2602281697121815",
|
"version": "0.0.0",
|
||||||
"data_folder_path": "/home/user/.local/share/net.akkudoktoreos.net",
|
"data_folder_path": "/home/user/.local/share/net.akkudoktoreos.net",
|
||||||
"data_output_subpath": "output",
|
"data_output_subpath": "output",
|
||||||
"latitude": 52.52,
|
"latitude": 52.52,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
| latitude | `EOS_GENERAL__LATITUDE` | `Optional[float]` | `rw` | `52.52` | Latitude in decimal degrees between -90 and 90. North is positive (ISO 19115) (°) |
|
| 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 (°) |
|
| longitude | `EOS_GENERAL__LONGITUDE` | `Optional[float]` | `rw` | `13.405` | Longitude in decimal degrees within -180 to 180 (°) |
|
||||||
| timezone | | `Optional[str]` | `ro` | `N/A` | Computed timezone based on latitude and longitude. |
|
| timezone | | `Optional[str]` | `ro` | `N/A` | Computed timezone based on latitude and longitude. |
|
||||||
| version | `EOS_GENERAL__VERSION` | `str` | `rw` | `0.2.0.dev2602281697121815` | Configuration file version. Used to check compatibility. |
|
| version | `EOS_GENERAL__VERSION` | `Optional[str]` | `rw` | `None` | Configuration file version. |
|
||||||
:::
|
:::
|
||||||
<!-- pyml enable line-length -->
|
<!-- pyml enable line-length -->
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"general": {
|
"general": {
|
||||||
"version": "0.2.0.dev2602281697121815",
|
"version": "0.0.0",
|
||||||
"data_folder_path": "/home/user/.local/share/net.akkudoktoreos.net",
|
"data_folder_path": "/home/user/.local/share/net.akkudoktoreos.net",
|
||||||
"data_output_subpath": "output",
|
"data_output_subpath": "output",
|
||||||
"latitude": 52.52,
|
"latitude": 52.52,
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"general": {
|
"general": {
|
||||||
"version": "0.2.0.dev2602281697121815",
|
"version": "0.0.0",
|
||||||
"data_folder_path": "/home/user/.local/share/net.akkudoktoreos.net",
|
"data_folder_path": "/home/user/.local/share/net.akkudoktoreos.net",
|
||||||
"data_output_subpath": "output",
|
"data_output_subpath": "output",
|
||||||
"latitude": 52.52,
|
"latitude": 52.52,
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
| ---- | ---- | --------- | ------- | ----------- |
|
| ---- | ---- | --------- | ------- | ----------- |
|
||||||
| generations | `Optional[int]` | `rw` | `400` | Number of generations to evaluate the optimal solution [>= 10]. Defaults to 400. |
|
| generations | `Optional[int]` | `rw` | `400` | Number of generations to evaluate the optimal solution [>= 10]. Defaults to 400. |
|
||||||
| individuals | `Optional[int]` | `rw` | `300` | Number of individuals (solutions) to generate for the (initial) generation [>= 10]. Defaults to 300. |
|
| individuals | `Optional[int]` | `rw` | `300` | Number of individuals (solutions) to generate for the (initial) generation [>= 10]. Defaults to 300. |
|
||||||
| penalties | `Optional[dict[str, Union[float, int, str]]]` | `rw` | `None` | A dictionary of penalty function parameters consisting of a penalty function parameter name and the associated value. |
|
| penalties | `dict[str, Union[float, int, str]]` | `rw` | `required` | A dictionary of penalty function parameters consisting of a penalty function parameter name and the associated value. |
|
||||||
| seed | `Optional[int]` | `rw` | `None` | Fixed seed for genetic algorithm. Defaults to 'None' which means random seed. |
|
| seed | `Optional[int]` | `rw` | `None` | Fixed seed for genetic algorithm. Defaults to 'None' which means random seed. |
|
||||||
:::
|
:::
|
||||||
<!-- pyml enable line-length -->
|
<!-- pyml enable line-length -->
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Akkudoktor-EOS
|
# Akkudoktor-EOS
|
||||||
|
|
||||||
**Version**: `v0.2.0.dev2602281697121815`
|
**Version**: `v0.2.0.dev2603032000228213`
|
||||||
|
|
||||||
<!-- pyml disable line-length -->
|
<!-- pyml disable line-length -->
|
||||||
**Description**: This project provides a comprehensive solution for simulating and optimizing an energy system based on renewable energy sources. With a focus on photovoltaic (PV) systems, battery storage (batteries), load management (consumer requirements), heat pumps, electric vehicles, and consideration of electricity price data, this system enables forecasting and optimization of energy flow and costs over a specified period.
|
**Description**: This project provides a comprehensive solution for simulating and optimizing an energy system based on renewable energy sources. With a focus on photovoltaic (PV) systems, battery storage (batteries), load management (consumer requirements), heat pumps, electric vehicles, and consideration of electricity price data, this system enables forecasting and optimization of energy flow and costs over a specified period.
|
||||||
|
|||||||
41
openapi.json
41
openapi.json
@@ -8,7 +8,7 @@
|
|||||||
"name": "Apache 2.0",
|
"name": "Apache 2.0",
|
||||||
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
|
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
|
||||||
},
|
},
|
||||||
"version": "v0.2.0.dev2602281697121815"
|
"version": "v0.2.0.dev2603032000228213"
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
"/v1/admin/cache/clear": {
|
"/v1/admin/cache/clear": {
|
||||||
@@ -4448,10 +4448,19 @@
|
|||||||
"description": "EOS is running as home assistant add-on."
|
"description": "EOS is running as home assistant add-on."
|
||||||
},
|
},
|
||||||
"version": {
|
"version": {
|
||||||
"type": "string",
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
"title": "Version",
|
"title": "Version",
|
||||||
"description": "Configuration file version. Used to check compatibility.",
|
"description": "Configuration file version.",
|
||||||
"default": "0.2.0.dev2602281697121815"
|
"examples": [
|
||||||
|
"0.0.0"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"data_folder_path": {
|
"data_folder_path": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -4511,10 +4520,19 @@
|
|||||||
"GeneralSettings-Output": {
|
"GeneralSettings-Output": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"version": {
|
"version": {
|
||||||
"type": "string",
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
"title": "Version",
|
"title": "Version",
|
||||||
"description": "Configuration file version. Used to check compatibility.",
|
"description": "Configuration file version.",
|
||||||
"default": "0.2.0.dev2602281697121815"
|
"examples": [
|
||||||
|
"0.0.0"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"data_folder_path": {
|
"data_folder_path": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -4685,8 +4703,6 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"penalties": {
|
"penalties": {
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -4700,12 +4716,7 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object",
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Penalties",
|
"title": "Penalties",
|
||||||
"description": "A dictionary of penalty function parameters consisting of a penalty function parameter name and the associated value.",
|
"description": "A dictionary of penalty function parameters consisting of a penalty function parameter name and the associated value.",
|
||||||
"examples": [
|
"examples": [
|
||||||
|
|||||||
@@ -129,11 +129,10 @@ class GeneralSettings(SettingsBaseModel):
|
|||||||
exclude=True,
|
exclude=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
version: str = Field(
|
version: Optional[str] = Field(
|
||||||
default=__version__,
|
default=None, # keep None here, will be set elsewhere
|
||||||
json_schema_extra={
|
json_schema_extra={"description": "Configuration file version."},
|
||||||
"description": "Configuration file version. Used to check compatibility."
|
examples=["0.0.0"],
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
data_folder_path: Path = Field(
|
data_folder_path: Path = Field(
|
||||||
@@ -195,18 +194,6 @@ class GeneralSettings(SettingsBaseModel):
|
|||||||
|
|
||||||
compatible_versions: ClassVar[list[str]] = [__version__]
|
compatible_versions: ClassVar[list[str]] = [__version__]
|
||||||
|
|
||||||
@field_validator("version")
|
|
||||||
@classmethod
|
|
||||||
def check_version(cls, v: str) -> str:
|
|
||||||
if v not in cls.compatible_versions:
|
|
||||||
error = (
|
|
||||||
f"Incompatible configuration version '{v}'. "
|
|
||||||
f"Expected: {', '.join(cls.compatible_versions)}."
|
|
||||||
)
|
|
||||||
logger.error(error)
|
|
||||||
raise ValueError(error)
|
|
||||||
return v
|
|
||||||
|
|
||||||
@field_validator("data_folder_path", mode="after")
|
@field_validator("data_folder_path", mode="after")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_data_folder_path(cls, value: Optional[Union[str, Path]]) -> Path:
|
def validate_data_folder_path(cls, value: Optional[Union[str, Path]]) -> Path:
|
||||||
@@ -886,6 +873,73 @@ class ConfigEOS(SingletonMixin, SettingsEOSDefaults):
|
|||||||
|
|
||||||
return config_file_path
|
return config_file_path
|
||||||
|
|
||||||
|
def to_config_json(self) -> str:
|
||||||
|
"""Serialize the configuration to a normalized JSON string.
|
||||||
|
|
||||||
|
The serialization routine ensures that the resulting JSON:
|
||||||
|
|
||||||
|
- Excludes computed fields.
|
||||||
|
- Excludes fields set to their default values.
|
||||||
|
- Excludes fields with value ``None``.
|
||||||
|
- Uses field aliases.
|
||||||
|
- Recursively removes empty dictionaries and lists.
|
||||||
|
- Ensures that ``general.version`` is always present and set
|
||||||
|
to the current application version.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: A normalized, human-readable JSON string representation
|
||||||
|
of the configuration.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If the serialized configuration root is not a dictionary.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def remove_empty(
|
||||||
|
obj: Union[dict[str, Any], list[Any], Any],
|
||||||
|
) -> Union[dict[str, Any], list[Any], Any]:
|
||||||
|
"""Recursively remove empty dictionaries, lists, and None values."""
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
cleaned: dict[str, Any] = {k: remove_empty(v) for k, v in obj.items()}
|
||||||
|
return {k: v for k, v in cleaned.items() if v not in (None, {}, [])}
|
||||||
|
elif isinstance(obj, list):
|
||||||
|
cleaned_list: list[Any] = [remove_empty(v) for v in obj]
|
||||||
|
return [v for v in cleaned_list if v not in (None, {}, [])]
|
||||||
|
else:
|
||||||
|
return obj
|
||||||
|
|
||||||
|
# Use model_dump_json to respect custom Pydantic serialization
|
||||||
|
json_str = self.model_dump_json(
|
||||||
|
exclude_computed_fields=True,
|
||||||
|
exclude_defaults=True,
|
||||||
|
exclude_none=True,
|
||||||
|
by_alias=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Load as JSON
|
||||||
|
root: Any = json.loads(json_str)
|
||||||
|
|
||||||
|
# Remove empty values recursively
|
||||||
|
cleaned_root = remove_empty(root)
|
||||||
|
|
||||||
|
# Validate that root is a dictionary
|
||||||
|
if not isinstance(cleaned_root, dict):
|
||||||
|
raise TypeError(
|
||||||
|
f"Configuration serialization error: root element must be a dictionary, "
|
||||||
|
f"got {type(cleaned_root).__name__}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure version is present and correct
|
||||||
|
cleaned_root.setdefault("general", {})
|
||||||
|
cleaned_root["general"]["version"] = __version__
|
||||||
|
|
||||||
|
# Return pretty-printed JSON
|
||||||
|
return json.dumps(
|
||||||
|
cleaned_root,
|
||||||
|
indent=4,
|
||||||
|
sort_keys=True,
|
||||||
|
ensure_ascii=False,
|
||||||
|
)
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
@@ -897,8 +951,7 @@ class ConfigEOS(SingletonMixin, SettingsEOSDefaults):
|
|||||||
if not self.general.config_file_path:
|
if not self.general.config_file_path:
|
||||||
raise ValueError("Configuration file path unknown.")
|
raise ValueError("Configuration file path unknown.")
|
||||||
with self.general.config_file_path.open("w", encoding="utf-8", newline="\n") as f_out:
|
with self.general.config_file_path.open("w", encoding="utf-8", newline="\n") as f_out:
|
||||||
json_str = super().model_dump_json(indent=4)
|
f_out.write(self.to_config_json())
|
||||||
f_out.write(json_str)
|
|
||||||
|
|
||||||
def update(self) -> None:
|
def update(self) -> None:
|
||||||
"""Updates all configuration fields.
|
"""Updates all configuration fields.
|
||||||
|
|||||||
@@ -2,11 +2,25 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
|
import types
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Set, Tuple, Union, cast
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
|
Callable,
|
||||||
|
Dict,
|
||||||
|
List,
|
||||||
|
Set,
|
||||||
|
Tuple,
|
||||||
|
Union,
|
||||||
|
cast,
|
||||||
|
get_args,
|
||||||
|
get_origin,
|
||||||
|
)
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from akkudoktoreos.core.pydantic import BaseModel
|
||||||
from akkudoktoreos.core.version import __version__
|
from akkudoktoreos.core.version import __version__
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -173,13 +187,27 @@ def migrate_config_data(config_data: Dict[str, Any]) -> "SettingsEOSDefaults":
|
|||||||
return new_config
|
return new_config
|
||||||
|
|
||||||
|
|
||||||
|
def remove_empty(obj: dict) -> dict:
|
||||||
|
"""Recursively remove empty dicts and lists from a dictionary or list."""
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
cleaned = {k: remove_empty(v) for k, v in obj.items()}
|
||||||
|
# Remove keys where value is None, empty dict, or empty list
|
||||||
|
return {k: v for k, v in cleaned.items() if v not in (None, {}, [])}
|
||||||
|
elif isinstance(obj, list):
|
||||||
|
cleaned = [remove_empty(v) for v in obj]
|
||||||
|
# Remove empty elements
|
||||||
|
return [v for v in cleaned if v not in (None, {}, [])]
|
||||||
|
else:
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
def migrate_config_file(config_file: Path, backup_file: Path) -> bool:
|
def migrate_config_file(config_file: Path, backup_file: Path) -> bool:
|
||||||
"""Migrate configuration file to the current version.
|
"""Migrate configuration file to the current version.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if up-to-date or successfully migrated, False on failure.
|
bool: True if up-to-date or successfully migrated, False on failure.
|
||||||
"""
|
"""
|
||||||
global migrated_source_paths, mapped_count, auto_count, skipped_paths
|
global migrated_source_paths, mapped_count, auto_count, skipped_paths, remove_empty
|
||||||
|
|
||||||
# Reset globals at the start of each migration
|
# Reset globals at the start of each migration
|
||||||
migrated_source_paths = set()
|
migrated_source_paths = set()
|
||||||
@@ -220,13 +248,26 @@ def migrate_config_file(config_file: Path, backup_file: Path) -> bool:
|
|||||||
f"Failed to backup existing config (replace: {e_replace}; copy: {e_copy}). Continuing without backup."
|
f"Failed to backup existing config (replace: {e_replace}; copy: {e_copy}). Continuing without backup."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from akkudoktoreos.config.config import SettingsEOSDefaults
|
||||||
|
|
||||||
|
# Strip computed fields
|
||||||
|
config_data = _strip_computed_fields(config_data, SettingsEOSDefaults)
|
||||||
|
|
||||||
# Migrate config data
|
# Migrate config data
|
||||||
new_config = migrate_config_data(config_data)
|
new_config = migrate_config_data(config_data)
|
||||||
|
|
||||||
# Write migrated configuration
|
# Write migrated configuration
|
||||||
try:
|
try:
|
||||||
with config_file.open("w", encoding="utf-8", newline=None) as f_out:
|
with config_file.open("w", encoding="utf-8", newline=None) as f_out:
|
||||||
json_str = new_config.model_dump_json(indent=4)
|
# Need model_dump_json (not model_dump) because of special serialisation.
|
||||||
|
json_str = new_config.model_dump_json(
|
||||||
|
exclude_computed_fields=True,
|
||||||
|
exclude_defaults=True,
|
||||||
|
exclude_none=True,
|
||||||
|
by_alias=True,
|
||||||
|
)
|
||||||
|
cleaned = remove_empty(json.loads(json_str))
|
||||||
|
json_str = json.dumps(cleaned, indent=4, ensure_ascii=False)
|
||||||
f_out.write(json_str)
|
f_out.write(json_str)
|
||||||
except Exception as e_write:
|
except Exception as e_write:
|
||||||
logger.error(f"Failed to write migrated configuration to '{config_file}': {e_write}")
|
logger.error(f"Failed to write migrated configuration to '{config_file}': {e_write}")
|
||||||
@@ -289,3 +330,93 @@ def _migrate_matching_fields(
|
|||||||
skipped_paths.append(full_path)
|
skipped_paths.append(full_path)
|
||||||
continue
|
continue
|
||||||
return count
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
def _unwrap_model_type(annotation: Any) -> type[BaseModel] | None:
|
||||||
|
"""Extract a BaseModel subclass from complex typing annotations.
|
||||||
|
|
||||||
|
Supports:
|
||||||
|
- Optional[T]
|
||||||
|
- T | None
|
||||||
|
- Union[T, ...]
|
||||||
|
- Annotated[T, ...]
|
||||||
|
"""
|
||||||
|
origin = get_origin(annotation)
|
||||||
|
|
||||||
|
# Direct BaseModel
|
||||||
|
if isinstance(annotation, type) and issubclass(annotation, BaseModel):
|
||||||
|
return annotation
|
||||||
|
|
||||||
|
# Handle Union / Optional / |
|
||||||
|
if origin in (Union, types.UnionType):
|
||||||
|
for arg in get_args(annotation):
|
||||||
|
model = _unwrap_model_type(arg)
|
||||||
|
if model:
|
||||||
|
return model
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Handle Annotated[T, ...]
|
||||||
|
if origin is not None:
|
||||||
|
for arg in get_args(annotation):
|
||||||
|
model = _unwrap_model_type(arg)
|
||||||
|
if model:
|
||||||
|
return model
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_computed_fields(
|
||||||
|
data: Any,
|
||||||
|
model: type[BaseModel],
|
||||||
|
) -> Any:
|
||||||
|
"""Recursively remove computed fields from input data.
|
||||||
|
|
||||||
|
This removes only fields declared via `@computed_field`
|
||||||
|
in the Pydantic model hierarchy.
|
||||||
|
|
||||||
|
Unknown fields are preserved to allow later migration handling.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Raw JSON-like data.
|
||||||
|
model: Target Pydantic model class.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Cleaned data structure.
|
||||||
|
"""
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return data
|
||||||
|
|
||||||
|
cleaned = dict(data)
|
||||||
|
|
||||||
|
# Remove computed fields at this level
|
||||||
|
for field_name in model.model_computed_fields:
|
||||||
|
cleaned.pop(field_name, None)
|
||||||
|
|
||||||
|
# Recurse into declared model fields
|
||||||
|
for field_name, field in model.model_fields.items():
|
||||||
|
if field_name not in cleaned:
|
||||||
|
continue
|
||||||
|
|
||||||
|
value = cleaned[field_name]
|
||||||
|
annotation = field.annotation
|
||||||
|
origin = get_origin(annotation)
|
||||||
|
|
||||||
|
# 1️⃣ Direct nested model or Optional/Union-wrapped model
|
||||||
|
nested_model = _unwrap_model_type(annotation)
|
||||||
|
if nested_model and isinstance(value, dict):
|
||||||
|
cleaned[field_name] = _strip_computed_fields(value, nested_model)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 2️⃣ List of models
|
||||||
|
if origin is list:
|
||||||
|
item_type = get_args(annotation)[0]
|
||||||
|
nested_model = _unwrap_model_type(item_type)
|
||||||
|
|
||||||
|
if nested_model and isinstance(value, list):
|
||||||
|
cleaned[field_name] = [
|
||||||
|
_strip_computed_fields(v, nested_model) if isinstance(v, dict) else v
|
||||||
|
for v in value
|
||||||
|
]
|
||||||
|
continue
|
||||||
|
|
||||||
|
return cleaned
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ and manipulation of configuration and generic data in a clear, scalable, and str
|
|||||||
|
|
||||||
import difflib
|
import difflib
|
||||||
import json
|
import json
|
||||||
|
import traceback
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from collections.abc import KeysView, MutableMapping
|
from collections.abc import KeysView, MutableMapping
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
@@ -1980,8 +1981,15 @@ class DataContainer(SingletonMixin, DataABC, MutableMapping):
|
|||||||
for provider in self.providers:
|
for provider in self.providers:
|
||||||
try:
|
try:
|
||||||
provider.update_data(force_enable=force_enable, force_update=force_update)
|
provider.update_data(force_enable=force_enable, force_update=force_update)
|
||||||
except Exception as ex:
|
except Exception as e:
|
||||||
error = f"Provider {provider.provider_id()} fails on update - enabled={provider.enabled()}, force_enable={force_enable}, force_update={force_update}: {ex}"
|
trace = "".join(traceback.TracebackException.from_exception(e).format())
|
||||||
|
error = (
|
||||||
|
f"Provider {provider.provider_id()} fails on update - "
|
||||||
|
f"enabled={provider.enabled()}, "
|
||||||
|
f"force_enable={force_enable}, "
|
||||||
|
f"force_update={force_update}"
|
||||||
|
f":\n{e}\n{trace}"
|
||||||
|
)
|
||||||
if provider.enabled():
|
if provider.enabled():
|
||||||
# The active provider failed — this is a real error worth propagating.
|
# The active provider failed — this is a real error worth propagating.
|
||||||
logger.error(error)
|
logger.error(error)
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ class EnergyManagement(
|
|||||||
cls.adapter.update_data(force_enable)
|
cls.adapter.update_data(force_enable)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
trace = "".join(traceback.TracebackException.from_exception(e).format())
|
trace = "".join(traceback.TracebackException.from_exception(e).format())
|
||||||
error_msg = f"Adapter update failed - phase {cls._stage}: {e}\n{trace}"
|
error_msg = f"Adapter update failed - phase {cls._stage}:\n{e}\n{trace}"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
|
|
||||||
cls._stage = EnergyManagementStage.FORECAST_RETRIEVAL
|
cls._stage = EnergyManagementStage.FORECAST_RETRIEVAL
|
||||||
@@ -292,7 +292,7 @@ class EnergyManagement(
|
|||||||
cls.adapter.update_data(force_enable)
|
cls.adapter.update_data(force_enable)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
trace = "".join(traceback.TracebackException.from_exception(e).format())
|
trace = "".join(traceback.TracebackException.from_exception(e).format())
|
||||||
error_msg = f"Adapter update failed - phase {cls._stage}: {e}\n{trace}"
|
error_msg = f"Adapter update failed - phase {cls._stage}:\n{e}\n{trace}"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
|
|
||||||
# Remember energy run datetime.
|
# Remember energy run datetime.
|
||||||
|
|||||||
@@ -218,17 +218,6 @@ class GeneticOptimizationParameters(
|
|||||||
if cls.config.optimization.genetic.generations is None:
|
if cls.config.optimization.genetic.generations is None:
|
||||||
logger.info("Genetic generations unknown - defaulting to 400.")
|
logger.info("Genetic generations unknown - defaulting to 400.")
|
||||||
cls.config.optimization.genetic.generations = 400
|
cls.config.optimization.genetic.generations = 400
|
||||||
if cls.config.optimization.genetic.penalties is None:
|
|
||||||
logger.info("Genetic penalties unknown - defaulting to demo config.")
|
|
||||||
cls.config.optimization.genetic.penalties = {"ev_soc_miss": 10}
|
|
||||||
if "ev_soc_miss" not in cls.config.optimization.genetic.penalties:
|
|
||||||
logger.info("ev_soc_miss penalty function parameter unknown - defaulting to 10.")
|
|
||||||
cls.config.optimization.genetic.penalties["ev_soc_miss"] = 10
|
|
||||||
if "ac_charge_break_even" not in cls.config.optimization.genetic.penalties:
|
|
||||||
# Default multiplier 1.0: penalty equals the exact economic loss in € from
|
|
||||||
# charging at a price that cannot be recovered given the round-trip efficiency
|
|
||||||
# and the best available future discharge price (after free PV energy is used).
|
|
||||||
cls.config.optimization.genetic.penalties["ac_charge_break_even"] = 1.0
|
|
||||||
|
|
||||||
# Get start solution from last run
|
# Get start solution from last run
|
||||||
start_solution = None
|
start_solution = None
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import Field, field_validator
|
from pydantic import Field, field_validator
|
||||||
@@ -460,91 +461,67 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel):
|
|||||||
)
|
)
|
||||||
pred = get_prediction()
|
pred = get_prediction()
|
||||||
|
|
||||||
if "pvforecast_ac_power" in pred.record_keys:
|
for pred_key, pred_fill_method, pred_solution_key, pred_solution_factor in [
|
||||||
prediction["pvforecast_ac_energy_wh"] = (
|
(
|
||||||
pred.key_to_array(
|
"pvforecast_ac_power",
|
||||||
key="pvforecast_ac_power",
|
"linear",
|
||||||
|
"pvforecast_ac_energy_wh",
|
||||||
|
power_to_energy_per_interval_factor,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"pvforecast_dc_power",
|
||||||
|
"linear",
|
||||||
|
"pvforecast_dc_energy_wh",
|
||||||
|
power_to_energy_per_interval_factor,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"elecprice_marketprice_wh",
|
||||||
|
"ffill",
|
||||||
|
"elec_price_amt_kwh",
|
||||||
|
1000.0,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"feed_in_tariff_wh",
|
||||||
|
"linear",
|
||||||
|
"feed_in_tariff_amt_kwh",
|
||||||
|
1000.0,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"weather_temp_air",
|
||||||
|
"linear",
|
||||||
|
"weather_air_temp_celcius",
|
||||||
|
1.0,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"loadforecast_power_w",
|
||||||
|
"linear",
|
||||||
|
"loadforecast_energy_wh",
|
||||||
|
power_to_energy_per_interval_factor,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"loadakkudoktor_std_power_w",
|
||||||
|
"linear",
|
||||||
|
"loadakkudoktor_std_energy_wh",
|
||||||
|
power_to_energy_per_interval_factor,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"loadakkudoktor_mean_power_w",
|
||||||
|
"linear",
|
||||||
|
"loadakkudoktor_mean_energy_wh",
|
||||||
|
power_to_energy_per_interval_factor,
|
||||||
|
),
|
||||||
|
]:
|
||||||
|
if pred_key in pred.record_keys:
|
||||||
|
array = pred.key_to_array(
|
||||||
|
key=pred_key,
|
||||||
start_datetime=start_datetime,
|
start_datetime=start_datetime,
|
||||||
end_datetime=end_datetime,
|
end_datetime=end_datetime,
|
||||||
interval=to_duration(f"{interval_hours} hours"),
|
interval=to_duration(f"{interval_hours} hours"),
|
||||||
fill_method="linear",
|
fill_method=pred_fill_method,
|
||||||
)
|
)
|
||||||
* power_to_energy_per_interval_factor
|
# 'key_to_array()' creates None values array if no data records are available.
|
||||||
).tolist()
|
if array is not None and array.size > 0 and not np.any(pd.isna(array)):
|
||||||
if "pvforecast_dc_power" in pred.record_keys:
|
prediction[pred_solution_key] = (array * pred_solution_factor).tolist()
|
||||||
prediction["pvforecast_dc_energy_wh"] = (
|
|
||||||
pred.key_to_array(
|
|
||||||
key="pvforecast_dc_power",
|
|
||||||
start_datetime=start_datetime,
|
|
||||||
end_datetime=end_datetime,
|
|
||||||
interval=to_duration(f"{interval_hours} hours"),
|
|
||||||
fill_method="linear",
|
|
||||||
)
|
|
||||||
* power_to_energy_per_interval_factor
|
|
||||||
).tolist()
|
|
||||||
if "elecprice_marketprice_wh" in pred.record_keys:
|
|
||||||
prediction["elec_price_amt_kwh"] = (
|
|
||||||
pred.key_to_array(
|
|
||||||
key="elecprice_marketprice_wh",
|
|
||||||
start_datetime=start_datetime,
|
|
||||||
end_datetime=end_datetime,
|
|
||||||
interval=to_duration(f"{interval_hours} hours"),
|
|
||||||
fill_method="ffill",
|
|
||||||
)
|
|
||||||
* 1000
|
|
||||||
).tolist()
|
|
||||||
if "feed_in_tariff_wh" in pred.record_keys:
|
|
||||||
prediction["feed_in_tariff_amt_kwh"] = (
|
|
||||||
pred.key_to_array(
|
|
||||||
key="feed_in_tariff_wh",
|
|
||||||
start_datetime=start_datetime,
|
|
||||||
end_datetime=end_datetime,
|
|
||||||
interval=to_duration(f"{interval_hours} hours"),
|
|
||||||
fill_method="linear",
|
|
||||||
)
|
|
||||||
* 1000
|
|
||||||
).tolist()
|
|
||||||
if "weather_temp_air" in pred.record_keys:
|
|
||||||
prediction["weather_air_temp_celcius"] = pred.key_to_array(
|
|
||||||
key="weather_temp_air",
|
|
||||||
start_datetime=start_datetime,
|
|
||||||
end_datetime=end_datetime,
|
|
||||||
interval=to_duration(f"{interval_hours} hours"),
|
|
||||||
fill_method="linear",
|
|
||||||
).tolist()
|
|
||||||
if "loadforecast_power_w" in pred.record_keys:
|
|
||||||
prediction["loadforecast_energy_wh"] = (
|
|
||||||
pred.key_to_array(
|
|
||||||
key="loadforecast_power_w",
|
|
||||||
start_datetime=start_datetime,
|
|
||||||
end_datetime=end_datetime,
|
|
||||||
interval=to_duration(f"{interval_hours} hours"),
|
|
||||||
fill_method="linear",
|
|
||||||
)
|
|
||||||
* power_to_energy_per_interval_factor
|
|
||||||
).tolist()
|
|
||||||
if "loadakkudoktor_std_power_w" in pred.record_keys:
|
|
||||||
prediction["loadakkudoktor_std_energy_wh"] = (
|
|
||||||
pred.key_to_array(
|
|
||||||
key="loadakkudoktor_std_power_w",
|
|
||||||
start_datetime=start_datetime,
|
|
||||||
end_datetime=end_datetime,
|
|
||||||
interval=to_duration(f"{interval_hours} hours"),
|
|
||||||
fill_method="linear",
|
|
||||||
)
|
|
||||||
* power_to_energy_per_interval_factor
|
|
||||||
).tolist()
|
|
||||||
if "loadakkudoktor_mean_power_w" in pred.record_keys:
|
|
||||||
prediction["loadakkudoktor_mean_energy_wh"] = (
|
|
||||||
pred.key_to_array(
|
|
||||||
key="loadakkudoktor_mean_power_w",
|
|
||||||
start_datetime=start_datetime,
|
|
||||||
end_datetime=end_datetime,
|
|
||||||
interval=to_duration(f"{interval_hours} hours"),
|
|
||||||
fill_method="linear",
|
|
||||||
)
|
|
||||||
* power_to_energy_per_interval_factor
|
|
||||||
).tolist()
|
|
||||||
|
|
||||||
optimization_solution = OptimizationSolution(
|
optimization_solution = OptimizationSolution(
|
||||||
id=f"optimization-genetic@{to_datetime(as_string=True)}",
|
id=f"optimization-genetic@{to_datetime(as_string=True)}",
|
||||||
|
|||||||
@@ -41,8 +41,11 @@ class GeneticCommonSettings(SettingsBaseModel):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
penalties: Optional[dict[str, Union[float, int, str]]] = Field(
|
penalties: dict[str, Union[float, int, str]] = Field(
|
||||||
default=None,
|
default_factory=lambda: {
|
||||||
|
"ev_soc_miss": 10,
|
||||||
|
"ac_charge_break_even": 1.0,
|
||||||
|
},
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
"description": "A dictionary of penalty function parameters consisting of a penalty function parameter name and the associated value.",
|
"description": "A dictionary of penalty function parameters consisting of a penalty function parameter name and the associated value.",
|
||||||
"examples": [
|
"examples": [
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ def fastapi_admin_database_stats_get() -> dict:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
trace = "".join(traceback.TracebackException.from_exception(e).format())
|
trace = "".join(traceback.TracebackException.from_exception(e).format())
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400, detail=f"Error on database statistic retrieval: {e}\n{trace}"
|
status_code=400, detail=f"Error on database statistic retrieval:\n{e}\n{trace}"
|
||||||
)
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@@ -538,7 +538,7 @@ def fastapi_config_put_key(
|
|||||||
trace = "".join(traceback.TracebackException.from_exception(e).format())
|
trace = "".join(traceback.TracebackException.from_exception(e).format())
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail=f"Error on update of configuration '{path}','{value}': {e}\n{trace}",
|
detail=f"Error on update of configuration '{path}','{value}':\n{e}\n{trace}",
|
||||||
)
|
)
|
||||||
|
|
||||||
return get_config()
|
return get_config()
|
||||||
@@ -937,7 +937,7 @@ async def fastapi_prediction_update(
|
|||||||
trace = "".join(traceback.TracebackException.from_exception(e).format())
|
trace = "".join(traceback.TracebackException.from_exception(e).format())
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail=f"Error on prediction update: {e}\n{trace}",
|
detail=f"Error on prediction update:\n{e}\n{trace}",
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response()
|
return Response()
|
||||||
@@ -972,7 +972,7 @@ async def fastapi_prediction_update_provider(
|
|||||||
trace = "".join(traceback.TracebackException.from_exception(e).format())
|
trace = "".join(traceback.TracebackException.from_exception(e).format())
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail=f"Error on prediction update: {e}\n{trace}",
|
detail=f"Error on prediction update:\n{e}\n{trace}",
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response()
|
return Response()
|
||||||
|
|||||||
99
tests/test_configfile.py
Normal file
99
tests/test_configfile.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from akkudoktoreos.config.config import ConfigEOS, GeneralSettings
|
||||||
|
|
||||||
|
|
||||||
|
class TestConfigEOSToConfigFile:
|
||||||
|
|
||||||
|
def test_to_config_file_writes_file(self, config_eos):
|
||||||
|
config_path = config_eos.general.config_file_path
|
||||||
|
|
||||||
|
# Remove file to test writing
|
||||||
|
config_path.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
config_eos.to_config_file()
|
||||||
|
|
||||||
|
assert config_path.exists()
|
||||||
|
assert config_path.read_text().strip().startswith("{")
|
||||||
|
|
||||||
|
def test_to_config_file_excludes_computed_fields(self, config_eos):
|
||||||
|
config_path = config_eos.general.config_file_path
|
||||||
|
|
||||||
|
config_eos.to_config_file()
|
||||||
|
data = json.loads(config_path.read_text())
|
||||||
|
|
||||||
|
assert "timezone" not in data["general"]
|
||||||
|
assert "data_output_path" not in data["general"]
|
||||||
|
assert "config_folder_path" not in data["general"]
|
||||||
|
assert "config_file_path" not in data["general"]
|
||||||
|
|
||||||
|
def test_to_config_file_excludes_defaults(self, config_eos):
|
||||||
|
"""Ensure fields with default values are excluded when saving config."""
|
||||||
|
|
||||||
|
# Pick fields that have defaults
|
||||||
|
default_latitude = GeneralSettings.model_fields["latitude"].default
|
||||||
|
default_longitude = GeneralSettings.model_fields["longitude"].default
|
||||||
|
|
||||||
|
# Ensure fields are at default values
|
||||||
|
config_eos.general.latitude = default_latitude
|
||||||
|
config_eos.general.longitude = default_longitude
|
||||||
|
|
||||||
|
# Save the config using the correct path managed by config_eos
|
||||||
|
config_eos.to_config_file()
|
||||||
|
|
||||||
|
# Read back JSON from the correct path
|
||||||
|
config_file_path = config_eos.general.config_file_path
|
||||||
|
content = json.loads(config_file_path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
# Default fields should not appear
|
||||||
|
assert "latitude" not in content["general"]
|
||||||
|
assert "longitude" not in content["general"]
|
||||||
|
|
||||||
|
# Non-default value should appear
|
||||||
|
config_eos.general.latitude = 48.0
|
||||||
|
config_eos.to_config_file()
|
||||||
|
content = json.loads(config_file_path.read_text(encoding="utf-8"))
|
||||||
|
assert content["general"]["latitude"] == 48.0
|
||||||
|
|
||||||
|
def test_to_config_file_excludes_none_fields(self, config_eos):
|
||||||
|
config_eos.general.latitude = None
|
||||||
|
|
||||||
|
config_path = config_eos.general.config_file_path
|
||||||
|
config_eos.to_config_file()
|
||||||
|
|
||||||
|
data = json.loads(config_path.read_text())
|
||||||
|
|
||||||
|
assert "latitude" not in data["general"]
|
||||||
|
|
||||||
|
def test_to_config_file_includes_version(tmp_path, config_eos):
|
||||||
|
"""Ensure general.version is always included."""
|
||||||
|
# Save config
|
||||||
|
config_eos.to_config_file()
|
||||||
|
|
||||||
|
# Read back JSON
|
||||||
|
config_file_path = config_eos.general.config_file_path
|
||||||
|
content = json.loads(config_file_path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
# Assert 'version' is included even if default
|
||||||
|
assert content["general"]["version"] == config_eos.general.version
|
||||||
|
|
||||||
|
def test_to_config_file_roundtrip(self, config_eos):
|
||||||
|
config_eos.merge_settings_from_dict(
|
||||||
|
{
|
||||||
|
"general": {"latitude": 48.0},
|
||||||
|
"server": {"port": 9000},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
config_path = config_eos.general.config_file_path
|
||||||
|
config_eos.to_config_file()
|
||||||
|
|
||||||
|
raw_data = json.loads(config_path.read_text())
|
||||||
|
reloaded = ConfigEOS.model_validate(raw_data)
|
||||||
|
|
||||||
|
assert reloaded.general.latitude == 48.0
|
||||||
|
assert reloaded.server.port == 9000
|
||||||
@@ -22,6 +22,10 @@ MIGRATION_PAIRS = [
|
|||||||
DIR_TESTDATA / "eos_config_andreas_0_1_0.json",
|
DIR_TESTDATA / "eos_config_andreas_0_1_0.json",
|
||||||
DIR_TESTDATA / "eos_config_andreas_now.json",
|
DIR_TESTDATA / "eos_config_andreas_now.json",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
DIR_TESTDATA / "eos_config_unstripped.json",
|
||||||
|
DIR_TESTDATA / "eos_config_stripped.json",
|
||||||
|
),
|
||||||
# Add more pairs here:
|
# Add more pairs here:
|
||||||
# (DIR_TESTDATA / "old_config_X.json", DIR_TESTDATA / "expected_config_X.json"),
|
# (DIR_TESTDATA / "old_config_X.json", DIR_TESTDATA / "expected_config_X.json"),
|
||||||
]
|
]
|
||||||
@@ -124,16 +128,18 @@ class TestConfigMigration:
|
|||||||
new_model = SettingsEOSDefaults(**migrated_data)
|
new_model = SettingsEOSDefaults(**migrated_data)
|
||||||
assert isinstance(new_model, SettingsEOSDefaults)
|
assert isinstance(new_model, SettingsEOSDefaults)
|
||||||
|
|
||||||
def test_migrate_config_file_already_current(self, tmp_path: Path):
|
def test_migrate_config_file_already_current(self, tmp_config_file: Path):
|
||||||
"""Test that a current config file returns True immediately."""
|
"""Test that a current config file returns True immediately."""
|
||||||
config_path = tmp_path / "EOS_current.json"
|
backup_file = tmp_config_file.with_suffix(".bak")
|
||||||
default = SettingsEOSDefaults()
|
|
||||||
with config_path.open("w", encoding="utf-8") as f:
|
|
||||||
f.write(default.model_dump_json(indent=4))
|
|
||||||
|
|
||||||
backup_file = config_path.with_suffix(".bak")
|
# Run migration
|
||||||
|
result = configmigrate.migrate_config_file(tmp_config_file, backup_file)
|
||||||
|
assert result is True, "Migration should succeed even from invalid version."
|
||||||
|
|
||||||
result = configmigrate.migrate_config_file(config_path, backup_file)
|
backup_file.unlink()
|
||||||
|
assert not backup_file.exists()
|
||||||
|
|
||||||
|
result = configmigrate.migrate_config_file(tmp_config_file, backup_file)
|
||||||
assert result is True
|
assert result is True
|
||||||
assert not backup_file.exists(), "No backup should be made if config is already current."
|
assert not backup_file.exists(), "No backup should be made if config is already current."
|
||||||
|
|
||||||
|
|||||||
67
tests/testdata/eos_config_andreas_now.json
vendored
67
tests/testdata/eos_config_andreas_now.json
vendored
@@ -1,17 +1,12 @@
|
|||||||
{
|
{
|
||||||
"general": {
|
"general": {
|
||||||
"data_folder_path": "__ANY__",
|
"version": "__ANY__",
|
||||||
"data_output_subpath": "output",
|
"data_output_subpath": "output",
|
||||||
"latitude": 52.5,
|
"latitude": 52.5,
|
||||||
"longitude": 13.4
|
"longitude": 13.4
|
||||||
},
|
},
|
||||||
"cache": {
|
"cache": {
|
||||||
"subpath": "cache",
|
"subpath": "cache"
|
||||||
"cleanup_interval": 300.0
|
|
||||||
},
|
|
||||||
"ems": {
|
|
||||||
"startup_delay": 5.0,
|
|
||||||
"interval": 300.0
|
|
||||||
},
|
},
|
||||||
"logging": {
|
"logging": {
|
||||||
"console_level": "INFO"
|
"console_level": "INFO"
|
||||||
@@ -20,36 +15,30 @@
|
|||||||
"batteries": [
|
"batteries": [
|
||||||
{
|
{
|
||||||
"device_id": "pv_akku",
|
"device_id": "pv_akku",
|
||||||
"capacity_wh": 30000,
|
"capacity_wh": 30000
|
||||||
"charging_efficiency": 0.88,
|
|
||||||
"discharging_efficiency": 0.88,
|
|
||||||
"max_charge_power_w": 5000,
|
|
||||||
"min_soc_percentage": 0,
|
|
||||||
"max_soc_percentage": 100
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"electric_vehicles": [
|
"electric_vehicles": [
|
||||||
{
|
{
|
||||||
"charge_rates": [0.0, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0]
|
"charge_rates": [
|
||||||
|
0.0,
|
||||||
|
0.375,
|
||||||
|
0.5,
|
||||||
|
0.625,
|
||||||
|
0.75,
|
||||||
|
0.875,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"inverters": [],
|
|
||||||
"home_appliances": []
|
|
||||||
},
|
},
|
||||||
"measurement": {
|
"measurement": {
|
||||||
"load_emr_keys": ["Household"]
|
"load_emr_keys": [
|
||||||
|
"Household"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"optimization": {
|
"optimization": {
|
||||||
"horizon_hours": 48,
|
"horizon_hours": 48
|
||||||
"genetic": {
|
|
||||||
"penalties": {
|
|
||||||
"ev_soc_miss": 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"prediction": {
|
|
||||||
"hours": 48,
|
|
||||||
"historic_hours": 48
|
|
||||||
},
|
},
|
||||||
"elecprice": {
|
"elecprice": {
|
||||||
"provider": "ElecPriceAkkudoktor",
|
"provider": "ElecPriceAkkudoktor",
|
||||||
@@ -57,7 +46,7 @@
|
|||||||
},
|
},
|
||||||
"load": {
|
"load": {
|
||||||
"loadakkudoktor": {
|
"loadakkudoktor": {
|
||||||
"loadakkudoktor_year_energy_kwh": 13000
|
"loadakkudoktor_year_energy_kwh": 13000.0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pvforecast": {
|
"pvforecast": {
|
||||||
@@ -66,17 +55,16 @@
|
|||||||
{
|
{
|
||||||
"surface_tilt": 87.907,
|
"surface_tilt": 87.907,
|
||||||
"surface_azimuth": 175.0,
|
"surface_azimuth": 175.0,
|
||||||
"userhorizon": [28.0, 34.0, 32.0, 60.0],
|
"userhorizon": [
|
||||||
"peakpower": 13.110,
|
28.0,
|
||||||
"pvtechchoice": "crystSi",
|
34.0,
|
||||||
"mountingplace": "free",
|
32.0,
|
||||||
|
60.0
|
||||||
|
],
|
||||||
|
"peakpower": 13.11,
|
||||||
"loss": 18.6,
|
"loss": 18.6,
|
||||||
"trackingtype": 0,
|
"trackingtype": 0,
|
||||||
"optimal_surface_tilt": false,
|
|
||||||
"optimalangles": false,
|
|
||||||
"albedo": 0.25,
|
"albedo": 0.25,
|
||||||
"module_model": null,
|
|
||||||
"inverter_model": null,
|
|
||||||
"inverter_paco": 15000,
|
"inverter_paco": 15000,
|
||||||
"modules_per_string": 20,
|
"modules_per_string": 20,
|
||||||
"strings_per_inverter": 2
|
"strings_per_inverter": 2
|
||||||
@@ -88,11 +76,8 @@
|
|||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"host": "0.0.0.0",
|
"host": "0.0.0.0",
|
||||||
"port": 8503,
|
|
||||||
"verbose": true,
|
"verbose": true,
|
||||||
"startup_eosdash": true,
|
|
||||||
"eosdash_host": "0.0.0.0",
|
"eosdash_host": "0.0.0.0",
|
||||||
"eosdash_port": 8504
|
"eosdash_port": 8504
|
||||||
},
|
}
|
||||||
"utils": {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
27
tests/testdata/eos_config_minimal_now.json
vendored
27
tests/testdata/eos_config_minimal_now.json
vendored
@@ -1,28 +1,19 @@
|
|||||||
{
|
{
|
||||||
"elecprice": {
|
|
||||||
"charges_kwh": 0.21,
|
|
||||||
"provider": "ElecPriceImport"
|
|
||||||
},
|
|
||||||
"prediction": {
|
|
||||||
"historic_hours": 48,
|
|
||||||
"hours": 48
|
|
||||||
},
|
|
||||||
"optimization": {
|
|
||||||
"horizon_hours": 48,
|
|
||||||
"algorithm": "GENETIC",
|
|
||||||
"genetic": {
|
|
||||||
"individuals": 300,
|
|
||||||
"generations": 400
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"general": {
|
"general": {
|
||||||
|
"version": "__ANY__",
|
||||||
|
"data_output_subpath": "output",
|
||||||
"latitude": 52.5,
|
"latitude": 52.5,
|
||||||
"longitude": 13.4
|
"longitude": 13.4
|
||||||
},
|
},
|
||||||
|
"optimization": {
|
||||||
|
"horizon_hours": 48
|
||||||
|
},
|
||||||
|
"elecprice": {
|
||||||
|
"provider": "ElecPriceImport",
|
||||||
|
"charges_kwh": 0.21
|
||||||
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"startup_eosdash": true,
|
|
||||||
"host": "0.0.0.0",
|
"host": "0.0.0.0",
|
||||||
"port": 8503,
|
|
||||||
"eosdash_host": "0.0.0.0",
|
"eosdash_host": "0.0.0.0",
|
||||||
"eosdash_port": 8504
|
"eosdash_port": 8504
|
||||||
}
|
}
|
||||||
|
|||||||
134
tests/testdata/eos_config_stripped.json
vendored
Normal file
134
tests/testdata/eos_config_stripped.json
vendored
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
{
|
||||||
|
"general": {
|
||||||
|
"version": "__ANY__",
|
||||||
|
"data_output_subpath": "output"
|
||||||
|
},
|
||||||
|
"cache": {
|
||||||
|
"subpath": "cache"
|
||||||
|
},
|
||||||
|
"ems": {
|
||||||
|
"mode": "OPTIMIZATION"
|
||||||
|
},
|
||||||
|
"devices": {
|
||||||
|
"batteries": [
|
||||||
|
{
|
||||||
|
"device_id": "battery1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"max_batteries": 1,
|
||||||
|
"electric_vehicles": [
|
||||||
|
{
|
||||||
|
"device_id": "ev11",
|
||||||
|
"capacity_wh": 50000,
|
||||||
|
"min_soc_percentage": 70
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"max_electric_vehicles": 1,
|
||||||
|
"inverters": [
|
||||||
|
{
|
||||||
|
"device_id": "inverter1",
|
||||||
|
"max_power_w": 10000.0,
|
||||||
|
"battery_id": "battery1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"max_inverters": 1,
|
||||||
|
"home_appliances": [
|
||||||
|
{
|
||||||
|
"device_id": "dishwasher1",
|
||||||
|
"consumption_wh": 2000,
|
||||||
|
"duration_h": 3,
|
||||||
|
"time_windows": {
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"start_time": "08:00:00.000000 Europe/Berlin",
|
||||||
|
"duration": "5 hours"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"start_time": "15:00:00.000000 Europe/Berlin",
|
||||||
|
"duration": "3 hours"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"max_home_appliances": 1
|
||||||
|
},
|
||||||
|
"elecprice": {
|
||||||
|
"provider": "ElecPriceAkkudoktor"
|
||||||
|
},
|
||||||
|
"feedintariff": {
|
||||||
|
"provider": "FeedInTariffFixed",
|
||||||
|
"provider_settings": {
|
||||||
|
"FeedInTariffFixed": {
|
||||||
|
"feed_in_tariff_kwh": 0.078
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"load": {
|
||||||
|
"provider": "LoadAkkudoktorAdjusted",
|
||||||
|
"loadakkudoktor": {
|
||||||
|
"loadakkudoktor_year_energy_kwh": 3000.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pvforecast": {
|
||||||
|
"provider": "PVForecastAkkudoktor",
|
||||||
|
"planes": [
|
||||||
|
{
|
||||||
|
"surface_tilt": 7.0,
|
||||||
|
"surface_azimuth": 170.0,
|
||||||
|
"userhorizon": [
|
||||||
|
20.0,
|
||||||
|
27.0,
|
||||||
|
22.0,
|
||||||
|
20.0
|
||||||
|
],
|
||||||
|
"peakpower": 5.0,
|
||||||
|
"inverter_paco": 10000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"surface_tilt": 7.0,
|
||||||
|
"surface_azimuth": 90.0,
|
||||||
|
"userhorizon": [
|
||||||
|
30.0,
|
||||||
|
30.0,
|
||||||
|
30.0,
|
||||||
|
50.0
|
||||||
|
],
|
||||||
|
"peakpower": 4.8,
|
||||||
|
"inverter_paco": 10000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"surface_tilt": 60.0,
|
||||||
|
"surface_azimuth": 140.0,
|
||||||
|
"userhorizon": [
|
||||||
|
60.0,
|
||||||
|
30.0,
|
||||||
|
0.0,
|
||||||
|
30.0
|
||||||
|
],
|
||||||
|
"peakpower": 1.4,
|
||||||
|
"inverter_paco": 2000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"surface_tilt": 45.0,
|
||||||
|
"surface_azimuth": 185.0,
|
||||||
|
"userhorizon": [
|
||||||
|
45.0,
|
||||||
|
25.0,
|
||||||
|
30.0,
|
||||||
|
60.0
|
||||||
|
],
|
||||||
|
"peakpower": 1.6,
|
||||||
|
"inverter_paco": 1400
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"max_planes": 4
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"provider": "BrightSky"
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"eosdash_host": "127.0.0.1",
|
||||||
|
"eosdash_port": 8504
|
||||||
|
}
|
||||||
|
}
|
||||||
596
tests/testdata/eos_config_unstripped.json
vendored
Normal file
596
tests/testdata/eos_config_unstripped.json
vendored
Normal file
@@ -0,0 +1,596 @@
|
|||||||
|
{
|
||||||
|
"general": {
|
||||||
|
"version": "0.2.0.dev2603031877440961",
|
||||||
|
"data_folder_path": "/home/bobby/.local/share/net.akkudoktor.eos",
|
||||||
|
"data_output_subpath": "output",
|
||||||
|
"latitude": 52.52,
|
||||||
|
"longitude": 13.405,
|
||||||
|
"timezone": "Europe/Berlin",
|
||||||
|
"data_output_path": "/home/bobby/.local/share/net.akkudoktor.eos/output",
|
||||||
|
"config_folder_path": "/home/bobby/.config/net.akkudoktor.eos",
|
||||||
|
"config_file_path": "/home/bobby/.config/net.akkudoktor.eos/EOS.config.json"
|
||||||
|
},
|
||||||
|
"cache": {
|
||||||
|
"subpath": "cache",
|
||||||
|
"cleanup_interval": 300
|
||||||
|
},
|
||||||
|
"database": {
|
||||||
|
"provider": null,
|
||||||
|
"compression_level": 9,
|
||||||
|
"initial_load_window_h": null,
|
||||||
|
"keep_duration_h": null,
|
||||||
|
"autosave_interval_sec": 10,
|
||||||
|
"compaction_interval_sec": 604800,
|
||||||
|
"batch_size": 100,
|
||||||
|
"providers": [
|
||||||
|
"LMDB",
|
||||||
|
"SQLite"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ems": {
|
||||||
|
"startup_delay": 5,
|
||||||
|
"interval": 300,
|
||||||
|
"mode": "OPTIMIZATION"
|
||||||
|
},
|
||||||
|
"logging": {
|
||||||
|
"console_level": null,
|
||||||
|
"file_level": null,
|
||||||
|
"file_path": "/home/bobby/.local/share/net.akkudoktor.eos/output/eos.log"
|
||||||
|
},
|
||||||
|
"devices": {
|
||||||
|
"batteries": [
|
||||||
|
{
|
||||||
|
"device_id": "battery1",
|
||||||
|
"capacity_wh": 8000,
|
||||||
|
"charging_efficiency": 0.88,
|
||||||
|
"discharging_efficiency": 0.88,
|
||||||
|
"levelized_cost_of_storage_kwh": 0,
|
||||||
|
"max_charge_power_w": 5000,
|
||||||
|
"min_charge_power_w": 50,
|
||||||
|
"charge_rates": [
|
||||||
|
0,
|
||||||
|
0.1,
|
||||||
|
0.2,
|
||||||
|
0.3,
|
||||||
|
0.4,
|
||||||
|
0.5,
|
||||||
|
0.6,
|
||||||
|
0.7,
|
||||||
|
0.8,
|
||||||
|
0.9,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"min_soc_percentage": 0,
|
||||||
|
"max_soc_percentage": 100,
|
||||||
|
"measurement_key_soc_factor": "battery1-soc-factor",
|
||||||
|
"measurement_key_power_l1_w": "battery1-power-l1-w",
|
||||||
|
"measurement_key_power_l2_w": "battery1-power-l2-w",
|
||||||
|
"measurement_key_power_l3_w": "battery1-power-l3-w",
|
||||||
|
"measurement_key_power_3_phase_sym_w": "battery1-power-3-phase-sym-w",
|
||||||
|
"measurement_keys": [
|
||||||
|
"battery1-soc-factor",
|
||||||
|
"battery1-power-l1-w",
|
||||||
|
"battery1-power-l2-w",
|
||||||
|
"battery1-power-l3-w",
|
||||||
|
"battery1-power-3-phase-sym-w"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"max_batteries": 1,
|
||||||
|
"electric_vehicles": [
|
||||||
|
{
|
||||||
|
"device_id": "ev11",
|
||||||
|
"capacity_wh": 50000,
|
||||||
|
"charging_efficiency": 0.88,
|
||||||
|
"discharging_efficiency": 0.88,
|
||||||
|
"levelized_cost_of_storage_kwh": 0,
|
||||||
|
"max_charge_power_w": 5000,
|
||||||
|
"min_charge_power_w": 50,
|
||||||
|
"charge_rates": [
|
||||||
|
0,
|
||||||
|
0.1,
|
||||||
|
0.2,
|
||||||
|
0.3,
|
||||||
|
0.4,
|
||||||
|
0.5,
|
||||||
|
0.6,
|
||||||
|
0.7,
|
||||||
|
0.8,
|
||||||
|
0.9,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"min_soc_percentage": 70,
|
||||||
|
"max_soc_percentage": 100,
|
||||||
|
"measurement_key_soc_factor": "ev11-soc-factor",
|
||||||
|
"measurement_key_power_l1_w": "ev11-power-l1-w",
|
||||||
|
"measurement_key_power_l2_w": "ev11-power-l2-w",
|
||||||
|
"measurement_key_power_l3_w": "ev11-power-l3-w",
|
||||||
|
"measurement_key_power_3_phase_sym_w": "ev11-power-3-phase-sym-w",
|
||||||
|
"measurement_keys": [
|
||||||
|
"ev11-soc-factor",
|
||||||
|
"ev11-power-l1-w",
|
||||||
|
"ev11-power-l2-w",
|
||||||
|
"ev11-power-l3-w",
|
||||||
|
"ev11-power-3-phase-sym-w"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"max_electric_vehicles": 1,
|
||||||
|
"inverters": [
|
||||||
|
{
|
||||||
|
"device_id": "inverter1",
|
||||||
|
"max_power_w": 10000,
|
||||||
|
"battery_id": "battery1",
|
||||||
|
"ac_to_dc_efficiency": 1,
|
||||||
|
"dc_to_ac_efficiency": 1,
|
||||||
|
"max_ac_charge_power_w": null,
|
||||||
|
"measurement_keys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"max_inverters": 1,
|
||||||
|
"home_appliances": [
|
||||||
|
{
|
||||||
|
"device_id": "dishwasher1",
|
||||||
|
"consumption_wh": 2000,
|
||||||
|
"duration_h": 3,
|
||||||
|
"time_windows": {
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"start_time": "08:00:00.000000 Europe/Berlin",
|
||||||
|
"duration": "5 hours",
|
||||||
|
"day_of_week": null,
|
||||||
|
"date": null,
|
||||||
|
"locale": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"start_time": "15:00:00.000000 Europe/Berlin",
|
||||||
|
"duration": "3 hours",
|
||||||
|
"day_of_week": null,
|
||||||
|
"date": null,
|
||||||
|
"locale": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"measurement_keys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"max_home_appliances": 1,
|
||||||
|
"measurement_keys": [
|
||||||
|
"battery1-soc-factor",
|
||||||
|
"battery1-power-l1-w",
|
||||||
|
"battery1-power-l2-w",
|
||||||
|
"battery1-power-l3-w",
|
||||||
|
"battery1-power-3-phase-sym-w",
|
||||||
|
"ev11-soc-factor",
|
||||||
|
"ev11-power-l1-w",
|
||||||
|
"ev11-power-l2-w",
|
||||||
|
"ev11-power-l3-w",
|
||||||
|
"ev11-power-3-phase-sym-w"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"measurement": {
|
||||||
|
"historic_hours": 17520,
|
||||||
|
"load_emr_keys": null,
|
||||||
|
"grid_export_emr_keys": null,
|
||||||
|
"grid_import_emr_keys": null,
|
||||||
|
"pv_production_emr_keys": null,
|
||||||
|
"keys": []
|
||||||
|
},
|
||||||
|
"optimization": {
|
||||||
|
"horizon_hours": 24,
|
||||||
|
"interval": 3600,
|
||||||
|
"algorithm": "GENETIC",
|
||||||
|
"genetic": {
|
||||||
|
"individuals": 300,
|
||||||
|
"generations": 400,
|
||||||
|
"seed": null,
|
||||||
|
"penalties": {
|
||||||
|
"ev_soc_miss": 10,
|
||||||
|
"ac_charge_break_even": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"keys": [
|
||||||
|
"battery1_fault_op_factor",
|
||||||
|
"battery1_fault_op_mode",
|
||||||
|
"battery1_forced_charge_op_factor",
|
||||||
|
"battery1_forced_charge_op_mode",
|
||||||
|
"battery1_forced_discharge_op_factor",
|
||||||
|
"battery1_forced_discharge_op_mode",
|
||||||
|
"battery1_frequency_regulation_op_factor",
|
||||||
|
"battery1_frequency_regulation_op_mode",
|
||||||
|
"battery1_grid_support_export_op_factor",
|
||||||
|
"battery1_grid_support_export_op_mode",
|
||||||
|
"battery1_grid_support_import_op_factor",
|
||||||
|
"battery1_grid_support_import_op_mode",
|
||||||
|
"battery1_idle_op_factor",
|
||||||
|
"battery1_idle_op_mode",
|
||||||
|
"battery1_non_export_op_factor",
|
||||||
|
"battery1_non_export_op_mode",
|
||||||
|
"battery1_outage_supply_op_factor",
|
||||||
|
"battery1_outage_supply_op_mode",
|
||||||
|
"battery1_peak_shaving_op_factor",
|
||||||
|
"battery1_peak_shaving_op_mode",
|
||||||
|
"battery1_ramp_rate_control_op_factor",
|
||||||
|
"battery1_ramp_rate_control_op_mode",
|
||||||
|
"battery1_reserve_backup_op_factor",
|
||||||
|
"battery1_reserve_backup_op_mode",
|
||||||
|
"battery1_self_consumption_op_factor",
|
||||||
|
"battery1_self_consumption_op_mode",
|
||||||
|
"battery1_soc_factor",
|
||||||
|
"costs_amt",
|
||||||
|
"date_time",
|
||||||
|
"ev11_fault_op_factor",
|
||||||
|
"ev11_fault_op_mode",
|
||||||
|
"ev11_forced_charge_op_factor",
|
||||||
|
"ev11_forced_charge_op_mode",
|
||||||
|
"ev11_forced_discharge_op_factor",
|
||||||
|
"ev11_forced_discharge_op_mode",
|
||||||
|
"ev11_frequency_regulation_op_factor",
|
||||||
|
"ev11_frequency_regulation_op_mode",
|
||||||
|
"ev11_grid_support_export_op_factor",
|
||||||
|
"ev11_grid_support_export_op_mode",
|
||||||
|
"ev11_grid_support_import_op_factor",
|
||||||
|
"ev11_grid_support_import_op_mode",
|
||||||
|
"ev11_idle_op_factor",
|
||||||
|
"ev11_idle_op_mode",
|
||||||
|
"ev11_non_export_op_factor",
|
||||||
|
"ev11_non_export_op_mode",
|
||||||
|
"ev11_outage_supply_op_factor",
|
||||||
|
"ev11_outage_supply_op_mode",
|
||||||
|
"ev11_peak_shaving_op_factor",
|
||||||
|
"ev11_peak_shaving_op_mode",
|
||||||
|
"ev11_ramp_rate_control_op_factor",
|
||||||
|
"ev11_ramp_rate_control_op_mode",
|
||||||
|
"ev11_reserve_backup_op_factor",
|
||||||
|
"ev11_reserve_backup_op_mode",
|
||||||
|
"ev11_self_consumption_op_factor",
|
||||||
|
"ev11_self_consumption_op_mode",
|
||||||
|
"ev11_soc_factor",
|
||||||
|
"genetic_ac_charge_factor",
|
||||||
|
"genetic_dc_charge_factor",
|
||||||
|
"genetic_discharge_allowed_factor",
|
||||||
|
"genetic_ev_charge_factor",
|
||||||
|
"grid_consumption_energy_wh",
|
||||||
|
"grid_feedin_energy_wh",
|
||||||
|
"homeappliance1_energy_wh",
|
||||||
|
"homeappliance1_off_op_factor",
|
||||||
|
"homeappliance1_off_op_mode",
|
||||||
|
"homeappliance1_run_op_factor",
|
||||||
|
"homeappliance1_run_op_mode",
|
||||||
|
"load_energy_wh",
|
||||||
|
"losses_energy_wh",
|
||||||
|
"revenue_amt"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"prediction": {
|
||||||
|
"hours": 48,
|
||||||
|
"historic_hours": 48
|
||||||
|
},
|
||||||
|
"elecprice": {
|
||||||
|
"provider": "ElecPriceAkkudoktor",
|
||||||
|
"charges_kwh": null,
|
||||||
|
"vat_rate": 1.19,
|
||||||
|
"elecpriceimport": {
|
||||||
|
"import_file_path": null,
|
||||||
|
"import_json": null
|
||||||
|
},
|
||||||
|
"energycharts": {
|
||||||
|
"bidding_zone": "DE-LU"
|
||||||
|
},
|
||||||
|
"providers": [
|
||||||
|
"ElecPriceAkkudoktor",
|
||||||
|
"ElecPriceEnergyCharts",
|
||||||
|
"ElecPriceImport"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"feedintariff": {
|
||||||
|
"provider": "FeedInTariffFixed",
|
||||||
|
"provider_settings": {
|
||||||
|
"FeedInTariffFixed": {
|
||||||
|
"feed_in_tariff_kwh": 0.078
|
||||||
|
},
|
||||||
|
"FeedInTariffImport": null
|
||||||
|
},
|
||||||
|
"providers": [
|
||||||
|
"FeedInTariffFixed",
|
||||||
|
"FeedInTariffImport"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"load": {
|
||||||
|
"provider": "LoadAkkudoktorAdjusted",
|
||||||
|
"loadakkudoktor": {
|
||||||
|
"loadakkudoktor_year_energy_kwh": 3000
|
||||||
|
},
|
||||||
|
"loadvrm": {
|
||||||
|
"load_vrm_token": "your-token",
|
||||||
|
"load_vrm_idsite": 12345
|
||||||
|
},
|
||||||
|
"loadimport": {
|
||||||
|
"import_file_path": null,
|
||||||
|
"import_json": null
|
||||||
|
},
|
||||||
|
"providers": [
|
||||||
|
"LoadAkkudoktor",
|
||||||
|
"LoadAkkudoktorAdjusted",
|
||||||
|
"LoadVrm",
|
||||||
|
"LoadImport"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pvforecast": {
|
||||||
|
"provider": "PVForecastAkkudoktor",
|
||||||
|
"provider_settings": {
|
||||||
|
"PVForecastImport": null,
|
||||||
|
"PVForecastVrm": null
|
||||||
|
},
|
||||||
|
"planes": [
|
||||||
|
{
|
||||||
|
"surface_tilt": 7,
|
||||||
|
"surface_azimuth": 170,
|
||||||
|
"userhorizon": [
|
||||||
|
20,
|
||||||
|
27,
|
||||||
|
22,
|
||||||
|
20
|
||||||
|
],
|
||||||
|
"peakpower": 5,
|
||||||
|
"pvtechchoice": "crystSi",
|
||||||
|
"mountingplace": "free",
|
||||||
|
"loss": 14,
|
||||||
|
"trackingtype": null,
|
||||||
|
"optimal_surface_tilt": false,
|
||||||
|
"optimalangles": false,
|
||||||
|
"albedo": null,
|
||||||
|
"module_model": null,
|
||||||
|
"inverter_model": null,
|
||||||
|
"inverter_paco": 10000,
|
||||||
|
"modules_per_string": null,
|
||||||
|
"strings_per_inverter": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"surface_tilt": 7,
|
||||||
|
"surface_azimuth": 90,
|
||||||
|
"userhorizon": [
|
||||||
|
30,
|
||||||
|
30,
|
||||||
|
30,
|
||||||
|
50
|
||||||
|
],
|
||||||
|
"peakpower": 4.8,
|
||||||
|
"pvtechchoice": "crystSi",
|
||||||
|
"mountingplace": "free",
|
||||||
|
"loss": 14,
|
||||||
|
"trackingtype": null,
|
||||||
|
"optimal_surface_tilt": false,
|
||||||
|
"optimalangles": false,
|
||||||
|
"albedo": null,
|
||||||
|
"module_model": null,
|
||||||
|
"inverter_model": null,
|
||||||
|
"inverter_paco": 10000,
|
||||||
|
"modules_per_string": null,
|
||||||
|
"strings_per_inverter": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"surface_tilt": 60,
|
||||||
|
"surface_azimuth": 140,
|
||||||
|
"userhorizon": [
|
||||||
|
60,
|
||||||
|
30,
|
||||||
|
0,
|
||||||
|
30
|
||||||
|
],
|
||||||
|
"peakpower": 1.4,
|
||||||
|
"pvtechchoice": "crystSi",
|
||||||
|
"mountingplace": "free",
|
||||||
|
"loss": 14,
|
||||||
|
"trackingtype": null,
|
||||||
|
"optimal_surface_tilt": false,
|
||||||
|
"optimalangles": false,
|
||||||
|
"albedo": null,
|
||||||
|
"module_model": null,
|
||||||
|
"inverter_model": null,
|
||||||
|
"inverter_paco": 2000,
|
||||||
|
"modules_per_string": null,
|
||||||
|
"strings_per_inverter": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"surface_tilt": 45,
|
||||||
|
"surface_azimuth": 185,
|
||||||
|
"userhorizon": [
|
||||||
|
45,
|
||||||
|
25,
|
||||||
|
30,
|
||||||
|
60
|
||||||
|
],
|
||||||
|
"peakpower": 1.6,
|
||||||
|
"pvtechchoice": "crystSi",
|
||||||
|
"mountingplace": "free",
|
||||||
|
"loss": 14,
|
||||||
|
"trackingtype": null,
|
||||||
|
"optimal_surface_tilt": false,
|
||||||
|
"optimalangles": false,
|
||||||
|
"albedo": null,
|
||||||
|
"module_model": null,
|
||||||
|
"inverter_model": null,
|
||||||
|
"inverter_paco": 1400,
|
||||||
|
"modules_per_string": null,
|
||||||
|
"strings_per_inverter": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"max_planes": 4,
|
||||||
|
"providers": [
|
||||||
|
"PVForecastAkkudoktor",
|
||||||
|
"PVForecastVrm",
|
||||||
|
"PVForecastImport"
|
||||||
|
],
|
||||||
|
"planes_peakpower": [
|
||||||
|
5,
|
||||||
|
4.8,
|
||||||
|
1.4,
|
||||||
|
1.6
|
||||||
|
],
|
||||||
|
"planes_azimuth": [
|
||||||
|
170,
|
||||||
|
90,
|
||||||
|
140,
|
||||||
|
185
|
||||||
|
],
|
||||||
|
"planes_tilt": [
|
||||||
|
7,
|
||||||
|
7,
|
||||||
|
60,
|
||||||
|
45
|
||||||
|
],
|
||||||
|
"planes_userhorizon": [
|
||||||
|
[
|
||||||
|
20,
|
||||||
|
27,
|
||||||
|
22,
|
||||||
|
20
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30,
|
||||||
|
30,
|
||||||
|
30,
|
||||||
|
50
|
||||||
|
],
|
||||||
|
[
|
||||||
|
60,
|
||||||
|
30,
|
||||||
|
0,
|
||||||
|
30
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45,
|
||||||
|
25,
|
||||||
|
30,
|
||||||
|
60
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"planes_inverter_paco": [
|
||||||
|
10000,
|
||||||
|
10000,
|
||||||
|
2000,
|
||||||
|
1400
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"provider": "BrightSky",
|
||||||
|
"provider_settings": {
|
||||||
|
"WeatherImport": null
|
||||||
|
},
|
||||||
|
"providers": [
|
||||||
|
"BrightSky",
|
||||||
|
"ClearOutside",
|
||||||
|
"WeatherImport"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 8503,
|
||||||
|
"verbose": false,
|
||||||
|
"startup_eosdash": true,
|
||||||
|
"eosdash_host": "127.0.0.1",
|
||||||
|
"eosdash_port": 8504,
|
||||||
|
"eosdash_supervise_interval_sec": 10
|
||||||
|
},
|
||||||
|
"utils": {},
|
||||||
|
"adapter": {
|
||||||
|
"provider": null,
|
||||||
|
"homeassistant": {
|
||||||
|
"config_entity_ids": null,
|
||||||
|
"load_emr_entity_ids": null,
|
||||||
|
"grid_export_emr_entity_ids": null,
|
||||||
|
"grid_import_emr_entity_ids": null,
|
||||||
|
"pv_production_emr_entity_ids": null,
|
||||||
|
"device_measurement_entity_ids": null,
|
||||||
|
"device_instruction_entity_ids": null,
|
||||||
|
"solution_entity_ids": null,
|
||||||
|
"homeassistant_entity_ids": [],
|
||||||
|
"eos_solution_entity_ids": [
|
||||||
|
"sensor.eos_battery1_fault_op_factor",
|
||||||
|
"sensor.eos_battery1_fault_op_mode",
|
||||||
|
"sensor.eos_battery1_forced_charge_op_factor",
|
||||||
|
"sensor.eos_battery1_forced_charge_op_mode",
|
||||||
|
"sensor.eos_battery1_forced_discharge_op_factor",
|
||||||
|
"sensor.eos_battery1_forced_discharge_op_mode",
|
||||||
|
"sensor.eos_battery1_frequency_regulation_op_factor",
|
||||||
|
"sensor.eos_battery1_frequency_regulation_op_mode",
|
||||||
|
"sensor.eos_battery1_grid_support_export_op_factor",
|
||||||
|
"sensor.eos_battery1_grid_support_export_op_mode",
|
||||||
|
"sensor.eos_battery1_grid_support_import_op_factor",
|
||||||
|
"sensor.eos_battery1_grid_support_import_op_mode",
|
||||||
|
"sensor.eos_battery1_idle_op_factor",
|
||||||
|
"sensor.eos_battery1_idle_op_mode",
|
||||||
|
"sensor.eos_battery1_non_export_op_factor",
|
||||||
|
"sensor.eos_battery1_non_export_op_mode",
|
||||||
|
"sensor.eos_battery1_outage_supply_op_factor",
|
||||||
|
"sensor.eos_battery1_outage_supply_op_mode",
|
||||||
|
"sensor.eos_battery1_peak_shaving_op_factor",
|
||||||
|
"sensor.eos_battery1_peak_shaving_op_mode",
|
||||||
|
"sensor.eos_battery1_ramp_rate_control_op_factor",
|
||||||
|
"sensor.eos_battery1_ramp_rate_control_op_mode",
|
||||||
|
"sensor.eos_battery1_reserve_backup_op_factor",
|
||||||
|
"sensor.eos_battery1_reserve_backup_op_mode",
|
||||||
|
"sensor.eos_battery1_self_consumption_op_factor",
|
||||||
|
"sensor.eos_battery1_self_consumption_op_mode",
|
||||||
|
"sensor.eos_battery1_soc_factor",
|
||||||
|
"sensor.eos_costs_amt",
|
||||||
|
"sensor.eos_date_time",
|
||||||
|
"sensor.eos_ev11_fault_op_factor",
|
||||||
|
"sensor.eos_ev11_fault_op_mode",
|
||||||
|
"sensor.eos_ev11_forced_charge_op_factor",
|
||||||
|
"sensor.eos_ev11_forced_charge_op_mode",
|
||||||
|
"sensor.eos_ev11_forced_discharge_op_factor",
|
||||||
|
"sensor.eos_ev11_forced_discharge_op_mode",
|
||||||
|
"sensor.eos_ev11_frequency_regulation_op_factor",
|
||||||
|
"sensor.eos_ev11_frequency_regulation_op_mode",
|
||||||
|
"sensor.eos_ev11_grid_support_export_op_factor",
|
||||||
|
"sensor.eos_ev11_grid_support_export_op_mode",
|
||||||
|
"sensor.eos_ev11_grid_support_import_op_factor",
|
||||||
|
"sensor.eos_ev11_grid_support_import_op_mode",
|
||||||
|
"sensor.eos_ev11_idle_op_factor",
|
||||||
|
"sensor.eos_ev11_idle_op_mode",
|
||||||
|
"sensor.eos_ev11_non_export_op_factor",
|
||||||
|
"sensor.eos_ev11_non_export_op_mode",
|
||||||
|
"sensor.eos_ev11_outage_supply_op_factor",
|
||||||
|
"sensor.eos_ev11_outage_supply_op_mode",
|
||||||
|
"sensor.eos_ev11_peak_shaving_op_factor",
|
||||||
|
"sensor.eos_ev11_peak_shaving_op_mode",
|
||||||
|
"sensor.eos_ev11_ramp_rate_control_op_factor",
|
||||||
|
"sensor.eos_ev11_ramp_rate_control_op_mode",
|
||||||
|
"sensor.eos_ev11_reserve_backup_op_factor",
|
||||||
|
"sensor.eos_ev11_reserve_backup_op_mode",
|
||||||
|
"sensor.eos_ev11_self_consumption_op_factor",
|
||||||
|
"sensor.eos_ev11_self_consumption_op_mode",
|
||||||
|
"sensor.eos_ev11_soc_factor",
|
||||||
|
"sensor.eos_genetic_ac_charge_factor",
|
||||||
|
"sensor.eos_genetic_dc_charge_factor",
|
||||||
|
"sensor.eos_genetic_discharge_allowed_factor",
|
||||||
|
"sensor.eos_genetic_ev_charge_factor",
|
||||||
|
"sensor.eos_grid_consumption_energy_wh",
|
||||||
|
"sensor.eos_grid_feedin_energy_wh",
|
||||||
|
"sensor.eos_homeappliance1_energy_wh",
|
||||||
|
"sensor.eos_homeappliance1_off_op_factor",
|
||||||
|
"sensor.eos_homeappliance1_off_op_mode",
|
||||||
|
"sensor.eos_homeappliance1_run_op_factor",
|
||||||
|
"sensor.eos_homeappliance1_run_op_mode",
|
||||||
|
"sensor.eos_load_energy_wh",
|
||||||
|
"sensor.eos_losses_energy_wh",
|
||||||
|
"sensor.eos_revenue_amt"
|
||||||
|
],
|
||||||
|
"eos_device_instruction_entity_ids": [
|
||||||
|
"sensor.eos_battery1",
|
||||||
|
"sensor.eos_ev11",
|
||||||
|
"sensor.eos_homeappliance1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nodered": {
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 1880
|
||||||
|
},
|
||||||
|
"providers": [
|
||||||
|
"HomeAssistant",
|
||||||
|
"NodeRED"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
24
uv.lock
generated
24
uv.lock
generated
@@ -84,10 +84,10 @@ requires-dist = [
|
|||||||
{ name = "deap", specifier = "==1.4.3" },
|
{ name = "deap", specifier = "==1.4.3" },
|
||||||
{ name = "deprecated", marker = "extra == 'dev'", specifier = "==1.3.1" },
|
{ name = "deprecated", marker = "extra == 'dev'", specifier = "==1.3.1" },
|
||||||
{ name = "docutils", marker = "extra == 'dev'", specifier = "==0.21.2" },
|
{ name = "docutils", marker = "extra == 'dev'", specifier = "==0.21.2" },
|
||||||
{ name = "fastapi", extras = ["standard-no-fastapi-cloud-cli"], specifier = "==0.134.0" },
|
{ name = "fastapi", extras = ["standard-no-fastapi-cloud-cli"], specifier = "==0.135.1" },
|
||||||
{ name = "fastapi-cli", specifier = "==0.0.24" },
|
{ name = "fastapi-cli", specifier = "==0.0.24" },
|
||||||
{ name = "gitpython", marker = "extra == 'dev'", specifier = "==3.1.46" },
|
{ name = "gitpython", marker = "extra == 'dev'", specifier = "==3.1.46" },
|
||||||
{ name = "linkify-it-py", specifier = "==2.0.3" },
|
{ name = "linkify-it-py", specifier = "==2.1.0" },
|
||||||
{ name = "lmdb", specifier = "==1.7.5" },
|
{ name = "lmdb", specifier = "==1.7.5" },
|
||||||
{ name = "loguru", specifier = "==0.7.3" },
|
{ name = "loguru", specifier = "==0.7.3" },
|
||||||
{ name = "markdown-it-py", specifier = "==4.0.0" },
|
{ name = "markdown-it-py", specifier = "==4.0.0" },
|
||||||
@@ -112,7 +112,7 @@ requires-dist = [
|
|||||||
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = "==1.3.0" },
|
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = "==1.3.0" },
|
||||||
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = "==7.0.0" },
|
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = "==7.0.0" },
|
||||||
{ name = "pytest-xprocess", marker = "extra == 'dev'", specifier = "==1.0.2" },
|
{ name = "pytest-xprocess", marker = "extra == 'dev'", specifier = "==1.0.2" },
|
||||||
{ name = "python-fasthtml", specifier = "==0.12.47" },
|
{ name = "python-fasthtml", specifier = "==0.12.48" },
|
||||||
{ name = "requests", specifier = "==2.32.5" },
|
{ name = "requests", specifier = "==2.32.5" },
|
||||||
{ name = "rich-toolkit", specifier = "==0.19.7" },
|
{ name = "rich-toolkit", specifier = "==0.19.7" },
|
||||||
{ name = "scipy", specifier = "==1.17.1" },
|
{ name = "scipy", specifier = "==1.17.1" },
|
||||||
@@ -828,7 +828,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi"
|
name = "fastapi"
|
||||||
version = "0.134.0"
|
version = "0.135.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "annotated-doc" },
|
{ name = "annotated-doc" },
|
||||||
@@ -837,9 +837,9 @@ dependencies = [
|
|||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
{ name = "typing-inspection" },
|
{ name = "typing-inspection" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/96/15/647ea81cb73b55b48fb095158a9cd64e42e9e4f1d34dbb5cc4a4939779d6/fastapi-0.134.0.tar.gz", hash = "sha256:3122b1ea0dbeaab48b5976e80b99ca7eda02be154bf03e126a33220e73255a9a", size = 385667, upload-time = "2026-02-27T21:18:12.931Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/e7/7b/f8e0211e9380f7195ba3f3d40c292594fd81ba8ec4629e3854c353aaca45/fastapi-0.135.1.tar.gz", hash = "sha256:d04115b508d936d254cea545b7312ecaa58a7b3a0f84952535b4c9afae7668cd", size = 394962, upload-time = "2026-03-01T18:18:29.369Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/e3/e6/fd49c28a54b7d6f5c64045155e40f6cff9ed4920055043fb5ac7969f7f2f/fastapi-0.134.0-py3-none-any.whl", hash = "sha256:f4e7214f24b2262258492e05c48cf21125e4ffc427e30dd32fb4f74049a3d56a", size = 110404, upload-time = "2026-02-27T21:18:10.809Z" },
|
{ url = "https://files.pythonhosted.org/packages/e4/72/42e900510195b23a56bde950d26a51f8b723846bfcaa0286e90287f0422b/fastapi-0.135.1-py3-none-any.whl", hash = "sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e", size = 116999, upload-time = "2026-03-01T18:18:30.831Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
@@ -1315,14 +1315,14 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linkify-it-py"
|
name = "linkify-it-py"
|
||||||
version = "2.0.3"
|
version = "2.1.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "uc-micro-py" },
|
{ name = "uc-micro-py" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/ae/bb56c6828e4797ba5a4821eec7c43b8bf40f69cda4d4f5f8c8a2810ec96a/linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048", size = 27946, upload-time = "2024-02-04T14:48:04.179Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79", size = 19820, upload-time = "2024-02-04T14:48:02.496Z" },
|
{ url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2449,7 +2449,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-fasthtml"
|
name = "python-fasthtml"
|
||||||
version = "0.12.47"
|
version = "0.12.48"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "beautifulsoup4" },
|
{ name = "beautifulsoup4" },
|
||||||
@@ -2463,9 +2463,9 @@ dependencies = [
|
|||||||
{ name = "starlette" },
|
{ name = "starlette" },
|
||||||
{ name = "uvicorn", extra = ["standard"] },
|
{ name = "uvicorn", extra = ["standard"] },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/47/afd5be266a7215921495553ad7afa26fa4c4a4b6e2f0d8f076f6098dfc1a/python_fasthtml-0.12.47.tar.gz", hash = "sha256:9efa6e1ff846e34889fcc4cbbab0b33b9e4d12c6a5d12aa1b8cf613675b7cee5", size = 71755, upload-time = "2026-02-21T02:20:52.171Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/02/20/545f9219e6212cb9813016dafc4f65de8f90f3b0b7113df0936b834bbecb/python_fasthtml-0.12.48.tar.gz", hash = "sha256:89b86391bd30bbd0edacc6806ceb9946442440e5fa0949d302232749ce3385d5", size = 71794, upload-time = "2026-03-02T17:59:49.171Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/cb/47/cdd57c2283f1072671d0ccf602d65564b289e92fba2edecb12541c830701/python_fasthtml-0.12.47-py3-none-any.whl", hash = "sha256:2189dfa0ec4bd04e01c1ad28d7ecf7b17ec50a1167f63fdbc9b398d4ad6b6145", size = 75500, upload-time = "2026-02-21T02:20:50.448Z" },
|
{ url = "https://files.pythonhosted.org/packages/24/97/4b98c5560342700b82588c5b670acb041842f9a7d7bf429e849a8b847251/python_fasthtml-0.12.48-py3-none-any.whl", hash = "sha256:124d3524005d3bf159880f26e6b7a6b998213c06c8af763801556b3a4612080d", size = 75512, upload-time = "2026-03-02T17:59:47.305Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
Reference in New Issue
Block a user