Rename settings variables (remove prefixes)

This commit is contained in:
Dominique Lasserre 2025-01-18 14:26:34 +01:00
parent 1e1bac9fdb
commit 3257dac92b
58 changed files with 867 additions and 918 deletions

3
.env
View File

@ -1,4 +1,5 @@
EOS_VERSION=main
EOS_PORT=8503
EOS_SERVER__PORT=8503
EOS_SERVER__EOSDASH_PORT=8504
PYTHON_VERSION=3.12.6

View File

@ -11,12 +11,14 @@ services:
dockerfile: "Dockerfile"
args:
PYTHON_VERSION: "${PYTHON_VERSION}"
env_file:
- .env
environment:
- EOS_CONFIG_DIR=config
- latitude=52.2
- longitude=13.4
- elecprice_provider=ElecPriceAkkudoktor
- elecprice_charges_kwh=0.21
- server_fasthtml_host=none
- EOS_PREDICTION__LATITUDE=52.2
- EOS_PREDICTION__LONGITUDE=13.4
- EOS_ELECPRICE__PROVIDER=ElecPriceAkkudoktor
- EOS_ELECPRICE__CHARGES_KWH=0.21
ports:
- "${EOS_PORT}:${EOS_PORT}"
- "${EOS_SERVER__PORT}:${EOS_SERVER__PORT}"
- "${EOS_SERVER__EOSDASH_PORT}:${EOS_SERVER__EOSDASH_PORT}"

View File

@ -55,8 +55,8 @@ General configuration to set directories of cache and output files.
| Name | Environment Variable | Type | Read-Only | Default | Description |
| ---- | -------------------- | ---- | --------- | ------- | ----------- |
| logging_level_default | `EOS_LOGGING__LOGGING_LEVEL_DEFAULT` | `Optional[str]` | `rw` | `None` | EOS default logging level. |
| logging_level_root | | `str` | `ro` | `N/A` | Root logger logging level. |
| level | `EOS_LOGGING__LEVEL` | `Optional[str]` | `rw` | `None` | EOS default logging level. |
| root_level | | `str` | `ro` | `N/A` | Root logger logging level. |
:::
### Example Input
@ -66,7 +66,7 @@ General configuration to set directories of cache and output files.
{
"logging": {
"logging_level_default": "INFO"
"level": "INFO"
}
}
```
@ -78,8 +78,8 @@ General configuration to set directories of cache and output files.
{
"logging": {
"logging_level_default": "INFO",
"logging_level_root": "INFO"
"level": "INFO",
"root_level": "INFO"
}
}
```
@ -167,7 +167,7 @@ General configuration to set directories of cache and output files.
| device_id | `str` | `rw` | `required` | ID of inverter |
| hours | `Optional[int]` | `rw` | `None` | Number of prediction hours. Defaults to global config prediction hours. |
| max_power_wh | `float` | `rw` | `required` | - |
| battery | `Optional[str]` | `rw` | `None` | ID of battery |
| battery_id | `Optional[str]` | `rw` | `None` | ID of battery |
:::
#### Example Input/Output
@ -182,7 +182,7 @@ General configuration to set directories of cache and output files.
"device_id": "inverter1",
"hours": null,
"max_power_wh": 10000.0,
"battery": null
"battery_id": null
}
]
}
@ -240,11 +240,11 @@ General configuration to set directories of cache and output files.
| Name | Environment Variable | Type | Read-Only | Default | Description |
| ---- | -------------------- | ---- | --------- | ------- | ----------- |
| measurement_load0_name | `EOS_MEASUREMENT__MEASUREMENT_LOAD0_NAME` | `Optional[str]` | `rw` | `None` | Name of the load0 source |
| measurement_load1_name | `EOS_MEASUREMENT__MEASUREMENT_LOAD1_NAME` | `Optional[str]` | `rw` | `None` | Name of the load1 source |
| measurement_load2_name | `EOS_MEASUREMENT__MEASUREMENT_LOAD2_NAME` | `Optional[str]` | `rw` | `None` | Name of the load2 source |
| measurement_load3_name | `EOS_MEASUREMENT__MEASUREMENT_LOAD3_NAME` | `Optional[str]` | `rw` | `None` | Name of the load3 source |
| measurement_load4_name | `EOS_MEASUREMENT__MEASUREMENT_LOAD4_NAME` | `Optional[str]` | `rw` | `None` | Name of the load4 source |
| load0_name | `EOS_MEASUREMENT__LOAD0_NAME` | `Optional[str]` | `rw` | `None` | Name of the load0 source |
| load1_name | `EOS_MEASUREMENT__LOAD1_NAME` | `Optional[str]` | `rw` | `None` | Name of the load1 source |
| load2_name | `EOS_MEASUREMENT__LOAD2_NAME` | `Optional[str]` | `rw` | `None` | Name of the load2 source |
| load3_name | `EOS_MEASUREMENT__LOAD3_NAME` | `Optional[str]` | `rw` | `None` | Name of the load3 source |
| load4_name | `EOS_MEASUREMENT__LOAD4_NAME` | `Optional[str]` | `rw` | `None` | Name of the load4 source |
:::
### Example Input/Output
@ -254,11 +254,11 @@ General configuration to set directories of cache and output files.
{
"measurement": {
"measurement_load0_name": "Household",
"measurement_load1_name": null,
"measurement_load2_name": null,
"measurement_load3_name": null,
"measurement_load4_name": null
"load0_name": "Household",
"load1_name": null,
"load2_name": null,
"load3_name": null,
"load4_name": null
}
}
```
@ -266,7 +266,7 @@ General configuration to set directories of cache and output files.
## General Optimization Configuration
Attributes:
optimization_hours (int): Number of hours for optimizations.
hours (int): Number of hours for optimizations.
:::{table} optimization
:widths: 10 20 10 5 5 30
@ -274,9 +274,9 @@ Attributes:
| Name | Environment Variable | Type | Read-Only | Default | Description |
| ---- | -------------------- | ---- | --------- | ------- | ----------- |
| optimization_hours | `EOS_OPTIMIZATION__OPTIMIZATION_HOURS` | `Optional[int]` | `rw` | `48` | Number of hours into the future for optimizations. |
| optimization_penalty | `EOS_OPTIMIZATION__OPTIMIZATION_PENALTY` | `Optional[int]` | `rw` | `10` | Penalty factor used in optimization. |
| optimization_ev_available_charge_rates_percent | `EOS_OPTIMIZATION__OPTIMIZATION_EV_AVAILABLE_CHARGE_RATES_PERCENT` | `Optional[List[float]]` | `rw` | `[0.0, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0]` | Charge rates available for the EV in percent of maximum charge. |
| hours | `EOS_OPTIMIZATION__HOURS` | `Optional[int]` | `rw` | `48` | Number of hours into the future for optimizations. |
| penalty | `EOS_OPTIMIZATION__PENALTY` | `Optional[int]` | `rw` | `10` | Penalty factor used in optimization. |
| ev_available_charge_rates_percent | `EOS_OPTIMIZATION__EV_AVAILABLE_CHARGE_RATES_PERCENT` | `Optional[List[float]]` | `rw` | `[0.0, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0]` | Charge rates available for the EV in percent of maximum charge. |
:::
### Example Input/Output
@ -286,9 +286,9 @@ Attributes:
{
"optimization": {
"optimization_hours": 48,
"optimization_penalty": 10,
"optimization_ev_available_charge_rates_percent": [
"hours": 48,
"penalty": 10,
"ev_available_charge_rates_percent": [
0.0,
0.375,
0.5,
@ -309,9 +309,9 @@ Validators ensure each parameter is within a specified range. A computed propert
determines the time zone based on latitude and longitude.
Attributes:
prediction_hours (Optional[int]): Number of hours into the future for predictions.
hours (Optional[int]): Number of hours into the future for predictions.
Must be non-negative.
prediction_historic_hours (Optional[int]): Number of hours into the past for historical data.
historic_hours (Optional[int]): Number of hours into the past for historical data.
Must be non-negative.
latitude (Optional[float]): Latitude in degrees, must be between -90 and 90.
longitude (Optional[float]): Longitude in degrees, must be between -180 and 180.
@ -321,8 +321,8 @@ Properties:
and longitude.
Validators:
validate_prediction_hours (int): Ensures `prediction_hours` is a non-negative integer.
validate_prediction_historic_hours (int): Ensures `prediction_historic_hours` is a non-negative integer.
validate_hours (int): Ensures `hours` is a non-negative integer.
validate_historic_hours (int): Ensures `historic_hours` is a non-negative integer.
validate_latitude (float): Ensures `latitude` is within the range -90 to 90.
validate_longitude (float): Ensures `longitude` is within the range -180 to 180.
@ -332,8 +332,8 @@ Validators:
| Name | Environment Variable | Type | Read-Only | Default | Description |
| ---- | -------------------- | ---- | --------- | ------- | ----------- |
| prediction_hours | `EOS_PREDICTION__PREDICTION_HOURS` | `Optional[int]` | `rw` | `48` | Number of hours into the future for predictions |
| prediction_historic_hours | `EOS_PREDICTION__PREDICTION_HISTORIC_HOURS` | `Optional[int]` | `rw` | `48` | Number of hours into the past for historical predictions data |
| hours | `EOS_PREDICTION__HOURS` | `Optional[int]` | `rw` | `48` | Number of hours into the future for predictions |
| historic_hours | `EOS_PREDICTION__HISTORIC_HOURS` | `Optional[int]` | `rw` | `48` | Number of hours into the past for historical predictions data |
| latitude | `EOS_PREDICTION__LATITUDE` | `Optional[float]` | `rw` | `52.52` | Latitude in decimal degrees, between -90 and 90, north is positive (ISO 19115) (°) |
| longitude | `EOS_PREDICTION__LONGITUDE` | `Optional[float]` | `rw` | `13.405` | Longitude in decimal degrees, within -180 to 180 (°) |
| timezone | | `Optional[str]` | `ro` | `N/A` | Compute timezone based on latitude and longitude. |
@ -346,8 +346,8 @@ Validators:
{
"prediction": {
"prediction_hours": 48,
"prediction_historic_hours": 48,
"hours": 48,
"historic_hours": 48,
"latitude": 52.52,
"longitude": 13.405
}
@ -361,8 +361,8 @@ Validators:
{
"prediction": {
"prediction_hours": 48,
"prediction_historic_hours": 48,
"hours": 48,
"historic_hours": 48,
"latitude": 52.52,
"longitude": 13.405,
"timezone": "Europe/Berlin"
@ -378,8 +378,8 @@ Validators:
| Name | Environment Variable | Type | Read-Only | Default | Description |
| ---- | -------------------- | ---- | --------- | ------- | ----------- |
| elecprice_provider | `EOS_ELECPRICE__ELECPRICE_PROVIDER` | `Optional[str]` | `rw` | `None` | Electricity price provider id of provider to be used. |
| elecprice_charges_kwh | `EOS_ELECPRICE__ELECPRICE_CHARGES_KWH` | `Optional[float]` | `rw` | `None` | Electricity price charges (€/kWh). |
| provider | `EOS_ELECPRICE__PROVIDER` | `Optional[str]` | `rw` | `None` | Electricity price provider id of provider to be used. |
| charges_kwh | `EOS_ELECPRICE__CHARGES_KWH` | `Optional[float]` | `rw` | `None` | Electricity price charges (€/kWh). |
| provider_settings | `EOS_ELECPRICE__PROVIDER_SETTINGS` | `Optional[akkudoktoreos.prediction.elecpriceimport.ElecPriceImportCommonSettings]` | `rw` | `None` | Provider settings |
:::
@ -390,8 +390,8 @@ Validators:
{
"elecprice": {
"elecprice_provider": "ElecPriceAkkudoktor",
"elecprice_charges_kwh": 0.21,
"provider": "ElecPriceAkkudoktor",
"charges_kwh": 0.21,
"provider_settings": null
}
}
@ -405,8 +405,8 @@ Validators:
| Name | Type | Read-Only | Default | Description |
| ---- | ---- | --------- | ------- | ----------- |
| elecpriceimport_file_path | `Union[str, pathlib.Path, NoneType]` | `rw` | `None` | Path to the file to import elecprice data from. |
| elecpriceimport_json | `Optional[str]` | `rw` | `None` | JSON string, dictionary of electricity price forecast value lists. |
| import_file_path | `Union[str, pathlib.Path, NoneType]` | `rw` | `None` | Path to the file to import elecprice data from. |
| import_json | `Optional[str]` | `rw` | `None` | JSON string, dictionary of electricity price forecast value lists. |
:::
#### Example Input/Output
@ -417,8 +417,8 @@ Validators:
{
"elecprice": {
"provider_settings": {
"elecpriceimport_file_path": null,
"elecpriceimport_json": "{\"elecprice_marketprice_wh\": [0.0003384, 0.0003318, 0.0003284]}"
"import_file_path": null,
"import_json": "{\"elecprice_marketprice_wh\": [0.0003384, 0.0003318, 0.0003284]}"
}
}
}
@ -432,7 +432,7 @@ Validators:
| Name | Environment Variable | Type | Read-Only | Default | Description |
| ---- | -------------------- | ---- | --------- | ------- | ----------- |
| load_provider | `EOS_LOAD__LOAD_PROVIDER` | `Optional[str]` | `rw` | `None` | Load provider id of provider to be used. |
| provider | `EOS_LOAD__PROVIDER` | `Optional[str]` | `rw` | `None` | Load provider id of provider to be used. |
| provider_settings | `EOS_LOAD__PROVIDER_SETTINGS` | `Union[akkudoktoreos.prediction.loadakkudoktor.LoadAkkudoktorCommonSettings, akkudoktoreos.prediction.loadimport.LoadImportCommonSettings, NoneType]` | `rw` | `None` | Provider settings |
:::
@ -443,7 +443,7 @@ Validators:
{
"load": {
"load_provider": "LoadAkkudoktor",
"provider": "LoadAkkudoktor",
"provider_settings": null
}
}
@ -457,8 +457,8 @@ Validators:
| Name | Type | Read-Only | Default | Description |
| ---- | ---- | --------- | ------- | ----------- |
| load_import_file_path | `Union[str, pathlib.Path, NoneType]` | `rw` | `None` | Path to the file to import load data from. |
| load_import_json | `Optional[str]` | `rw` | `None` | JSON string, dictionary of load forecast value lists. |
| import_file_path | `Union[str, pathlib.Path, NoneType]` | `rw` | `None` | Path to the file to import load data from. |
| import_json | `Optional[str]` | `rw` | `None` | JSON string, dictionary of load forecast value lists. |
:::
#### Example Input/Output
@ -469,8 +469,8 @@ Validators:
{
"load": {
"provider_settings": {
"load_import_file_path": null,
"load_import_json": "{\"load0_mean\": [676.71, 876.19, 527.13]}"
"import_file_path": null,
"import_json": "{\"load0_mean\": [676.71, 876.19, 527.13]}"
}
}
}
@ -509,7 +509,7 @@ Validators:
| Name | Environment Variable | Type | Read-Only | Default | Description |
| ---- | -------------------- | ---- | --------- | ------- | ----------- |
| pvforecast_provider | `EOS_PVFORECAST__PVFORECAST_PROVIDER` | `Optional[str]` | `rw` | `None` | PVForecast provider id of provider to be used. |
| provider | `EOS_PVFORECAST__PROVIDER` | `Optional[str]` | `rw` | `None` | PVForecast provider id of provider to be used. |
| pvforecast0_surface_tilt | `EOS_PVFORECAST__PVFORECAST0_SURFACE_TILT` | `Optional[float]` | `rw` | `None` | Tilt angle from horizontal plane. Ignored for two-axis tracking. |
| pvforecast0_surface_azimuth | `EOS_PVFORECAST__PVFORECAST0_SURFACE_AZIMUTH` | `Optional[float]` | `rw` | `None` | Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270). |
| pvforecast0_userhorizon | `EOS_PVFORECAST__PVFORECAST0_USERHORIZON` | `Optional[List[float]]` | `rw` | `None` | Elevation of horizon in degrees, at equally spaced azimuth clockwise from north. |
@ -622,7 +622,7 @@ Validators:
{
"pvforecast": {
"pvforecast_provider": "PVForecastAkkudoktor",
"provider": "PVForecastAkkudoktor",
"pvforecast0_surface_tilt": 10.0,
"pvforecast0_surface_azimuth": 10.0,
"pvforecast0_userhorizon": [
@ -739,7 +739,7 @@ Validators:
{
"pvforecast": {
"pvforecast_provider": "PVForecastAkkudoktor",
"provider": "PVForecastAkkudoktor",
"pvforecast0_surface_tilt": 10.0,
"pvforecast0_surface_azimuth": 10.0,
"pvforecast0_userhorizon": [
@ -916,7 +916,7 @@ Validators:
| Name | Environment Variable | Type | Read-Only | Default | Description |
| ---- | -------------------- | ---- | --------- | ------- | ----------- |
| weather_provider | `EOS_WEATHER__WEATHER_PROVIDER` | `Optional[str]` | `rw` | `None` | Weather provider id of provider to be used. |
| provider | `EOS_WEATHER__PROVIDER` | `Optional[str]` | `rw` | `None` | Weather provider id of provider to be used. |
| provider_settings | `EOS_WEATHER__PROVIDER_SETTINGS` | `Optional[akkudoktoreos.prediction.weatherimport.WeatherImportCommonSettings]` | `rw` | `None` | Provider settings |
:::
@ -927,7 +927,7 @@ Validators:
{
"weather": {
"weather_provider": "WeatherImport",
"provider": "WeatherImport",
"provider_settings": null
}
}
@ -941,8 +941,8 @@ Validators:
| Name | Type | Read-Only | Default | Description |
| ---- | ---- | --------- | ------- | ----------- |
| weatherimport_file_path | `Union[str, pathlib.Path, NoneType]` | `rw` | `None` | Path to the file to import weather data from. |
| weatherimport_json | `Optional[str]` | `rw` | `None` | JSON string, dictionary of weather forecast value lists. |
| import_file_path | `Union[str, pathlib.Path, NoneType]` | `rw` | `None` | Path to the file to import weather data from. |
| import_json | `Optional[str]` | `rw` | `None` | JSON string, dictionary of weather forecast value lists. |
:::
#### Example Input/Output
@ -953,8 +953,8 @@ Validators:
{
"weather": {
"provider_settings": {
"weatherimport_file_path": null,
"weatherimport_json": "{\"weather_temp_air\": [18.3, 17.8, 16.9]}"
"import_file_path": null,
"import_json": "{\"weather_temp_air\": [18.3, 17.8, 16.9]}"
}
}
}
@ -971,12 +971,12 @@ Attributes:
| Name | Environment Variable | Type | Read-Only | Default | Description |
| ---- | -------------------- | ---- | --------- | ------- | ----------- |
| server_eos_host | `EOS_SERVER__SERVER_EOS_HOST` | `Optional[pydantic.networks.IPvAnyAddress]` | `rw` | `0.0.0.0` | EOS server IP address. |
| server_eos_port | `EOS_SERVER__SERVER_EOS_PORT` | `Optional[int]` | `rw` | `8503` | EOS server IP port number. |
| server_eos_verbose | `EOS_SERVER__SERVER_EOS_VERBOSE` | `Optional[bool]` | `rw` | `False` | Enable debug output |
| server_eos_startup_eosdash | `EOS_SERVER__SERVER_EOS_STARTUP_EOSDASH` | `Optional[bool]` | `rw` | `True` | EOS server to start EOSdash server. |
| server_eosdash_host | `EOS_SERVER__SERVER_EOSDASH_HOST` | `Optional[pydantic.networks.IPvAnyAddress]` | `rw` | `0.0.0.0` | EOSdash server IP address. |
| server_eosdash_port | `EOS_SERVER__SERVER_EOSDASH_PORT` | `Optional[int]` | `rw` | `8504` | EOSdash server IP port number. |
| host | `EOS_SERVER__HOST` | `Optional[pydantic.networks.IPvAnyAddress]` | `rw` | `0.0.0.0` | EOS server IP address. |
| port | `EOS_SERVER__PORT` | `Optional[int]` | `rw` | `8503` | EOS server IP port number. |
| verbose | `EOS_SERVER__VERBOSE` | `Optional[bool]` | `rw` | `False` | Enable debug output |
| startup_eosdash | `EOS_SERVER__STARTUP_EOSDASH` | `Optional[bool]` | `rw` | `True` | EOS server to start EOSdash server. |
| eosdash_host | `EOS_SERVER__EOSDASH_HOST` | `Optional[pydantic.networks.IPvAnyAddress]` | `rw` | `0.0.0.0` | EOSdash server IP address. |
| eosdash_port | `EOS_SERVER__EOSDASH_PORT` | `Optional[int]` | `rw` | `8504` | EOSdash server IP port number. |
:::
### Example Input/Output
@ -986,12 +986,12 @@ Attributes:
{
"server": {
"server_eos_host": "0.0.0.0",
"server_eos_port": 8503,
"server_eos_verbose": false,
"server_eos_startup_eosdash": true,
"server_eosdash_host": "0.0.0.0",
"server_eosdash_port": 8504
"host": "0.0.0.0",
"port": 8503,
"verbose": false,
"startup_eosdash": true,
"eosdash_host": "0.0.0.0",
"eosdash_port": 8504
}
}
```

View File

@ -63,7 +63,7 @@ Args:
year_energy (float): Yearly energy consumption in Wh.
Note:
Set LoadAkkudoktor as load_provider, then update data with
Set LoadAkkudoktor as provider, then update data with
'/v1/prediction/update'
and then request data with
'/v1/prediction/list?key=load_mean' instead.
@ -121,7 +121,7 @@ If no forecast values are available the missing ones at the start of the series
filled with the first available forecast value.
Note:
Set PVForecastAkkudoktor as pvforecast_provider, then update data with
Set PVForecastAkkudoktor as provider, then update data with
'/v1/prediction/update'
and then request data with
'/v1/prediction/list?key=pvforecast_ac_power' and
@ -151,7 +151,7 @@ Note:
Electricity price charges are added.
Note:
Set ElecPriceAkkudoktor as elecprice_provider, then update data with
Set ElecPriceAkkudoktor as provider, then update data with
'/v1/prediction/update'
and then request data with
'/v1/prediction/list?key=elecprice_marketprice_wh' or

View File

@ -55,7 +55,6 @@ following special environment variables:
- `EOS_CONFIG_DIR`: The directory to search for an EOS configuration file.
- `EOS_DIR`: The directory used by EOS for data, which will also be searched for an EOS
configuration file.
- `EOS_LOGGING_LEVEL`: The logging level to use in EOS.
### EOS Configuration File

View File

@ -56,21 +56,21 @@ A JSON string created from a [pandas](https://pandas.pydata.org/docs/index.html)
The EOS measurement store provides for storing meter readings of loads. There are currently five loads
foreseen. The associated `measurement key`s are:
- `measurement_load0_mr`: Load0 meter reading [kWh]
- `measurement_load1_mr`: Load1 meter reading [kWh]
- `measurement_load2_mr`: Load2 meter reading [kWh]
- `measurement_load3_mr`: Load3 meter reading [kWh]
- `measurement_load4_mr`: Load4 meter reading [kWh]
- `load0_mr`: Load0 meter reading [kWh]
- `load1_mr`: Load1 meter reading [kWh]
- `load2_mr`: Load2 meter reading [kWh]
- `load3_mr`: Load3 meter reading [kWh]
- `load4_mr`: Load4 meter reading [kWh]
For ease of use, you can assign descriptive names to the `measurement key`s to represent your
system's load sources. Use the following `configuration options` to set these names
(e.g., 'Dish Washer', 'Heat Pump'):
- `measurement_load0_name`: Name of the load0 source
- `measurement_load1_name`: Name of the load1 source
- `measurement_load2_name`: Name of the load2 source
- `measurement_load3_name`: Name of the load3 source
- `measurement_load4_name`: Name of the load4 source
- `load0_name`: Name of the load0 source
- `load1_name`: Name of the load1 source
- `load2_name`: Name of the load2 source
- `load3_name`: Name of the load3 source
- `load4_name`: Name of the load4 source
Load measurements can be stored for any datetime. The values between different meter readings are
linearly approximated. Since optimization occurs on the hour, storing values between hours is
@ -84,8 +84,8 @@ for specified intervals, usually one hour. This aggregated data can be used for
The EOS measurement store also allows for the storage of meter readings for grid import and export.
The associated `measurement key`s are:
- `measurement_grid_export_mr`: Export to grid meter reading [kWh]
- `measurement_grid_import_mr`: Import from grid meter reading [kWh]
- `grid_export_mr`: Export to grid meter reading [kWh]
- `grid_import_mr`: Import from grid meter reading [kWh]
:::{admonition} Todo
:class: note

View File

@ -22,7 +22,7 @@ Most predictions can be sourced from various providers. The specific provider to
in the EOS configuration. For example:
```python
weather_provider = "ClearOutside"
provider = "ClearOutside"
```
Some providers offer multiple prediction keys. For instance, a weather provider might provide data
@ -71,7 +71,7 @@ predictions are adjusted by real data from your system's measurements if given t
For example, the load prediction provider `LoadAkkudoktor` takes generic load data assembled by
Akkudoktor.net, maps that to the yearly energy consumption given in the configuration option
`loadakkudoktor_year_energy`, and finally adjusts the predicted load by the `measurement_loads`
`loadakkudoktor_year_energy`, and finally adjusts the predicted load by the `loads`
of your system.
## Prediction Updates
@ -107,26 +107,26 @@ Prediction keys:
Configuration options:
- `elecprice_provider`: Electricity price provider id of provider to be used.
- `provider`: Electricity price provider id of provider to be used.
- `ElecPriceAkkudoktor`: Retrieves from Akkudoktor.net.
- `ElecPriceImport`: Imports from a file or JSON string.
- `Akkudoktor`: Retrieves from Akkudoktor.net.
- `Import`: Imports from a file or JSON string.
- `elecprice_charges_kwh`: Electricity price charges (€/kWh).
- `elecpriceimport_file_path`: Path to the file to import electricity price forecast data from.
- `elecpriceimport_json`: JSON string, dictionary of electricity price forecast value lists.
- `charges_kwh`: Electricity price charges (€/kWh).
- `import_file_path`: Path to the file to import electricity price forecast data from.
- `import_json`: JSON string, dictionary of electricity price forecast value lists.
### ElecPriceAkkudoktor Provider
### Akkudoktor Provider
The `ElecPriceAkkudoktor` provider retrieves electricity prices directly from **Akkudoktor.net**,
The `Akkudoktor` provider retrieves electricity prices directly from **Akkudoktor.net**,
which supplies price data for the next 24 hours. For periods beyond 24 hours, the provider generates
prices by extrapolating historical price data combined with the most recent actual prices obtained
from Akkudoktor.net. Electricity price charges given in the `elecprice_charges_kwh` configuration
from Akkudoktor.net. Electricity price charges given in the `charges_kwh` configuration
option are added.
### ElecPriceImport Provider
### Import Provider
The `ElecPriceImport` provider is designed to import electricity prices from a file or a JSON
The `Import` provider is designed to import electricity prices from a file or a JSON
string. An external entity should update the file or JSON string whenever new prediction data
becomes available.
@ -136,7 +136,7 @@ The prediction key for the electricity price forecast data is:
The electricity proce forecast data must be provided in one of the formats described in
<project:#prediction-import-providers>. The data source must be given in the
`elecpriceimport_file_path` or `elecpriceimport_json` configuration option.
`import_file_path` or `import_json` configuration option.
## Load Prediction
@ -148,7 +148,7 @@ Prediction keys:
Configuration options:
- `load_provider`: Load provider id of provider to be used.
- `provider`: Load provider id of provider to be used.
- `LoadAkkudoktor`: Retrieves from local database.
- `LoadImport`: Imports from a file or JSON string.
@ -183,12 +183,12 @@ or `loadimport_json` configuration option.
Prediction keys:
- `pvforecast_ac_power`: Total DC power (W).
- `pvforecast_dc_power`: Total AC power (W).
- `ac_power`: Total DC power (W).
- `dc_power`: Total AC power (W).
Configuration options:
- `pvforecast_provider`: PVForecast provider id of provider to be used.
- `provider`: PVForecast provider id of provider to be used.
- `PVForecastAkkudoktor`: Retrieves from Akkudoktor.net.
- `PVForecastImport`: Imports from a file or JSON string.
@ -299,7 +299,7 @@ Example:
{
"latitude": 50.1234,
"longitude": 9.7654,
"pvforecast_provider": "PVForecastAkkudoktor",
"provider": "PVForecastAkkudoktor",
"pvforecast0_peakpower": 5.0,
"pvforecast0_surface_azimuth": -10,
"pvforecast0_surface_tilt": 7,
@ -332,8 +332,8 @@ becomes available.
The prediction keys for the PV forecast data are:
- `pvforecast_ac_power`: Total DC power (W).
- `pvforecast_dc_power`: Total AC power (W).
- `ac_power`: Total DC power (W).
- `dc_power`: Total AC power (W).
The PV forecast data must be provided in one of the formats described in
<project:#prediction-import-providers>. The data source must be given in the
@ -368,14 +368,14 @@ Prediction keys:
Configuration options:
- `weather_provider`: Load provider id of provider to be used.
- `provider`: Load provider id of provider to be used.
- `BrightSky`: Retrieves from https://api.brightsky.dev.
- `ClearOutside`: Retrieves from https://clearoutside.com/forecast.
- `LoadImport`: Imports from a file or JSON string.
- `weatherimport_file_path`: Path to the file to import weatherforecast data from.
- `weatherimport_json`: JSON string, dictionary of weather forecast value lists.
- `import_file_path`: Path to the file to import weatherforecast data from.
- `import_json`: JSON string, dictionary of weather forecast value lists.
### BrightSky Provider
@ -459,4 +459,4 @@ The prediction keys for the PV forecast data are:
The PV forecast data must be provided in one of the formats described in
<project:#prediction-import-providers>. The data source must be given in the
`weatherimport_file_path` or `pvforecastimport_json` configuration option.
`import_file_path` or `pvforecastimport_json` configuration option.

View File

@ -244,7 +244,7 @@
},
"ConfigEOS": {
"additionalProperties": false,
"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 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.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 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 ```",
"properties": {
"devices": {
"$ref": "#/components/schemas/DevicesCommonSettings",
@ -268,7 +268,7 @@
"logging": {
"$ref": "#/components/schemas/LoggingCommonSettings-Output",
"default": {
"logging_level_root": "INFO"
"root_level": "INFO"
}
},
"measurement": {
@ -278,7 +278,7 @@
"optimization": {
"$ref": "#/components/schemas/OptimizationCommonSettings",
"default": {
"optimization_ev_available_charge_rates_percent": [
"ev_available_charge_rates_percent": [
0.0,
0.375,
0.5,
@ -287,17 +287,17 @@
0.875,
1.0
],
"optimization_hours": 48,
"optimization_penalty": 10
"hours": 48,
"penalty": 10
}
},
"prediction": {
"$ref": "#/components/schemas/PredictionCommonSettings-Output",
"default": {
"historic_hours": 48,
"hours": 48,
"latitude": 52.52,
"longitude": 13.405,
"prediction_historic_hours": 48,
"prediction_hours": 48,
"timezone": "Europe/Berlin"
}
},
@ -345,12 +345,12 @@
"server": {
"$ref": "#/components/schemas/ServerCommonSettings",
"default": {
"server_eos_host": "0.0.0.0",
"server_eos_port": 8503,
"server_eos_startup_eosdash": true,
"server_eos_verbose": false,
"server_eosdash_host": "0.0.0.0",
"server_eosdash_port": 8504
"eosdash_host": "0.0.0.0",
"eosdash_port": 8504,
"host": "0.0.0.0",
"port": 8503,
"startup_eosdash": true,
"verbose": false
}
},
"utils": {
@ -434,7 +434,7 @@
"ElecPriceCommonSettings": {
"description": "Electricity Price Prediction Configuration.",
"properties": {
"elecprice_charges_kwh": {
"charges_kwh": {
"anyOf": [
{
"minimum": 0.0,
@ -448,9 +448,9 @@
"examples": [
0.21
],
"title": "Elecprice Charges Kwh"
"title": "Charges Kwh"
},
"elecprice_provider": {
"provider": {
"anyOf": [
{
"type": "string"
@ -463,7 +463,7 @@
"examples": [
"ElecPriceAkkudoktor"
],
"title": "Elecprice Provider"
"title": "Provider"
},
"provider_settings": {
"anyOf": [
@ -486,7 +486,7 @@
"ElecPriceImportCommonSettings": {
"description": "Common settings for elecprice data import from file or JSON String.",
"properties": {
"elecpriceimport_file_path": {
"import_file_path": {
"anyOf": [
{
"type": "string"
@ -504,9 +504,9 @@
null,
"/path/to/prices.json"
],
"title": "Elecpriceimport File Path"
"title": "Import File Path"
},
"elecpriceimport_json": {
"import_json": {
"anyOf": [
{
"type": "string"
@ -519,7 +519,7 @@
"examples": [
"{\"elecprice_marketprice_wh\": [0.0003384, 0.0003318, 0.0003284]}"
],
"title": "Elecpriceimport Json"
"title": "Import Json"
}
},
"title": "ElecPriceImportCommonSettings",
@ -900,7 +900,7 @@
"additionalProperties": false,
"description": "Inverter Device Simulation Configuration.",
"properties": {
"battery": {
"battery_id": {
"anyOf": [
{
"type": "string"
@ -914,7 +914,7 @@
null,
"battery1"
],
"title": "Battery"
"title": "Battery Id"
},
"device_id": {
"description": "ID of inverter",
@ -981,7 +981,7 @@
"LoadCommonSettings": {
"description": "Load Prediction Configuration.",
"properties": {
"load_provider": {
"provider": {
"anyOf": [
{
"type": "string"
@ -994,7 +994,7 @@
"examples": [
"LoadAkkudoktor"
],
"title": "Load Provider"
"title": "Provider"
},
"provider_settings": {
"anyOf": [
@ -1021,7 +1021,7 @@
"LoadImportCommonSettings": {
"description": "Common settings for load data import from file or JSON string.",
"properties": {
"load_import_file_path": {
"import_file_path": {
"anyOf": [
{
"type": "string"
@ -1039,9 +1039,9 @@
null,
"/path/to/yearly_load.json"
],
"title": "Load Import File Path"
"title": "Import File Path"
},
"load_import_json": {
"import_json": {
"anyOf": [
{
"type": "string"
@ -1054,7 +1054,7 @@
"examples": [
"{\"load0_mean\": [676.71, 876.19, 527.13]}"
],
"title": "Load Import Json"
"title": "Import Json"
}
},
"title": "LoadImportCommonSettings",
@ -1063,7 +1063,7 @@
"LoggingCommonSettings-Input": {
"description": "Logging Configuration.",
"properties": {
"logging_level_default": {
"level": {
"anyOf": [
{
"type": "string"
@ -1080,7 +1080,7 @@
"ERROR",
"CRITICAL"
],
"title": "Logging Level Default"
"title": "Level"
}
},
"title": "LoggingCommonSettings",
@ -1089,7 +1089,7 @@
"LoggingCommonSettings-Output": {
"description": "Logging Configuration.",
"properties": {
"logging_level_default": {
"level": {
"anyOf": [
{
"type": "string"
@ -1106,17 +1106,17 @@
"ERROR",
"CRITICAL"
],
"title": "Logging Level Default"
"title": "Level"
},
"logging_level_root": {
"root_level": {
"description": "Root logger logging level.",
"readOnly": true,
"title": "Logging Level Root",
"title": "Root Level",
"type": "string"
}
},
"required": [
"logging_level_root"
"root_level"
],
"title": "LoggingCommonSettings",
"type": "object"
@ -1124,7 +1124,7 @@
"MeasurementCommonSettings": {
"description": "Measurement Configuration.",
"properties": {
"measurement_load0_name": {
"load0_name": {
"anyOf": [
{
"type": "string"
@ -1138,9 +1138,9 @@
"Household",
"Heat Pump"
],
"title": "Measurement Load0 Name"
"title": "Load0 Name"
},
"measurement_load1_name": {
"load1_name": {
"anyOf": [
{
"type": "string"
@ -1153,9 +1153,9 @@
"examples": [
null
],
"title": "Measurement Load1 Name"
"title": "Load1 Name"
},
"measurement_load2_name": {
"load2_name": {
"anyOf": [
{
"type": "string"
@ -1168,9 +1168,9 @@
"examples": [
null
],
"title": "Measurement Load2 Name"
"title": "Load2 Name"
},
"measurement_load3_name": {
"load3_name": {
"anyOf": [
{
"type": "string"
@ -1183,9 +1183,9 @@
"examples": [
null
],
"title": "Measurement Load3 Name"
"title": "Load3 Name"
},
"measurement_load4_name": {
"load4_name": {
"anyOf": [
{
"type": "string"
@ -1198,16 +1198,16 @@
"examples": [
null
],
"title": "Measurement Load4 Name"
"title": "Load4 Name"
}
},
"title": "MeasurementCommonSettings",
"type": "object"
},
"OptimizationCommonSettings": {
"description": "General Optimization Configuration.\n\nAttributes:\n optimization_hours (int): Number of hours for optimizations.",
"description": "General Optimization Configuration.\n\nAttributes:\n hours (int): Number of hours for optimizations.",
"properties": {
"optimization_ev_available_charge_rates_percent": {
"ev_available_charge_rates_percent": {
"anyOf": [
{
"items": {
@ -1229,9 +1229,9 @@
1.0
],
"description": "Charge rates available for the EV in percent of maximum charge.",
"title": "Optimization Ev Available Charge Rates Percent"
"title": "Ev Available Charge Rates Percent"
},
"optimization_hours": {
"hours": {
"anyOf": [
{
"minimum": 0.0,
@ -1243,9 +1243,9 @@
],
"default": 48,
"description": "Number of hours into the future for optimizations.",
"title": "Optimization Hours"
"title": "Hours"
},
"optimization_penalty": {
"penalty": {
"anyOf": [
{
"type": "integer"
@ -1256,7 +1256,7 @@
],
"default": 10,
"description": "Penalty factor used in optimization.",
"title": "Optimization Penalty"
"title": "Penalty"
}
},
"title": "OptimizationCommonSettings",
@ -1453,6 +1453,21 @@
"PVForecastCommonSettings-Input": {
"description": "PV Forecast Configuration.",
"properties": {
"provider": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "PVForecast provider id of provider to be used.",
"examples": [
"PVForecastAkkudoktor"
],
"title": "Provider"
},
"provider_settings": {
"anyOf": [
{
@ -2949,8 +2964,15 @@
null
],
"title": "Pvforecast5 Userhorizon"
},
"pvforecast_provider": {
}
},
"title": "PVForecastCommonSettings",
"type": "object"
},
"PVForecastCommonSettings-Output": {
"description": "PV Forecast Configuration.",
"properties": {
"provider": {
"anyOf": [
{
"type": "string"
@ -2963,15 +2985,8 @@
"examples": [
"PVForecastAkkudoktor"
],
"title": "Pvforecast Provider"
}
},
"title": "PVForecastCommonSettings",
"type": "object"
},
"PVForecastCommonSettings-Output": {
"description": "PV Forecast Configuration.",
"properties": {
"title": "Provider"
},
"provider_settings": {
"anyOf": [
{
@ -4514,21 +4529,6 @@
"description": "Compute a list of the user horizon per active planes.",
"readOnly": true,
"title": "Pvforecast Planes Userhorizon"
},
"pvforecast_provider": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "PVForecast provider id of provider to be used.",
"examples": [
"PVForecastAkkudoktor"
],
"title": "Pvforecast Provider"
}
},
"required": [
@ -4585,8 +4585,36 @@
"type": "object"
},
"PredictionCommonSettings-Input": {
"description": "General Prediction Configuration.\n\nThis class provides configuration for prediction settings, allowing users to specify\nparameters such as the forecast duration (in hours) and location (latitude and longitude).\nValidators ensure each parameter is within a specified range. A computed property, `timezone`,\ndetermines the time zone based on latitude and longitude.\n\nAttributes:\n prediction_hours (Optional[int]): Number of hours into the future for predictions.\n Must be non-negative.\n prediction_historic_hours (Optional[int]): Number of hours into the past for historical data.\n Must be non-negative.\n latitude (Optional[float]): Latitude in degrees, must be between -90 and 90.\n longitude (Optional[float]): Longitude in degrees, must be between -180 and 180.\n\nProperties:\n timezone (Optional[str]): Computed time zone string based on the specified latitude\n and longitude.\n\nValidators:\n validate_prediction_hours (int): Ensures `prediction_hours` is a non-negative integer.\n validate_prediction_historic_hours (int): Ensures `prediction_historic_hours` is a non-negative integer.\n validate_latitude (float): Ensures `latitude` is within the range -90 to 90.\n validate_longitude (float): Ensures `longitude` is within the range -180 to 180.",
"description": "General Prediction Configuration.\n\nThis class provides configuration for prediction settings, allowing users to specify\nparameters such as the forecast duration (in hours) and location (latitude and longitude).\nValidators ensure each parameter is within a specified range. A computed property, `timezone`,\ndetermines the time zone based on latitude and longitude.\n\nAttributes:\n hours (Optional[int]): Number of hours into the future for predictions.\n Must be non-negative.\n historic_hours (Optional[int]): Number of hours into the past for historical data.\n Must be non-negative.\n latitude (Optional[float]): Latitude in degrees, must be between -90 and 90.\n longitude (Optional[float]): Longitude in degrees, must be between -180 and 180.\n\nProperties:\n timezone (Optional[str]): Computed time zone string based on the specified latitude\n and longitude.\n\nValidators:\n validate_hours (int): Ensures `hours` is a non-negative integer.\n validate_historic_hours (int): Ensures `historic_hours` is a non-negative integer.\n validate_latitude (float): Ensures `latitude` is within the range -90 to 90.\n validate_longitude (float): Ensures `longitude` is within the range -180 to 180.",
"properties": {
"historic_hours": {
"anyOf": [
{
"minimum": 0.0,
"type": "integer"
},
{
"type": "null"
}
],
"default": 48,
"description": "Number of hours into the past for historical predictions data",
"title": "Historic Hours"
},
"hours": {
"anyOf": [
{
"minimum": 0.0,
"type": "integer"
},
{
"type": "null"
}
],
"default": 48,
"description": "Number of hours into the future for predictions",
"title": "Hours"
},
"latitude": {
"anyOf": [
{
@ -4616,42 +4644,42 @@
"default": 13.405,
"description": "Longitude in decimal degrees, within -180 to 180 (\u00b0)",
"title": "Longitude"
},
"prediction_historic_hours": {
"anyOf": [
{
"minimum": 0.0,
"type": "integer"
},
{
"type": "null"
}
],
"default": 48,
"description": "Number of hours into the past for historical predictions data",
"title": "Prediction Historic Hours"
},
"prediction_hours": {
"anyOf": [
{
"minimum": 0.0,
"type": "integer"
},
{
"type": "null"
}
],
"default": 48,
"description": "Number of hours into the future for predictions",
"title": "Prediction Hours"
}
},
"title": "PredictionCommonSettings",
"type": "object"
},
"PredictionCommonSettings-Output": {
"description": "General Prediction Configuration.\n\nThis class provides configuration for prediction settings, allowing users to specify\nparameters such as the forecast duration (in hours) and location (latitude and longitude).\nValidators ensure each parameter is within a specified range. A computed property, `timezone`,\ndetermines the time zone based on latitude and longitude.\n\nAttributes:\n prediction_hours (Optional[int]): Number of hours into the future for predictions.\n Must be non-negative.\n prediction_historic_hours (Optional[int]): Number of hours into the past for historical data.\n Must be non-negative.\n latitude (Optional[float]): Latitude in degrees, must be between -90 and 90.\n longitude (Optional[float]): Longitude in degrees, must be between -180 and 180.\n\nProperties:\n timezone (Optional[str]): Computed time zone string based on the specified latitude\n and longitude.\n\nValidators:\n validate_prediction_hours (int): Ensures `prediction_hours` is a non-negative integer.\n validate_prediction_historic_hours (int): Ensures `prediction_historic_hours` is a non-negative integer.\n validate_latitude (float): Ensures `latitude` is within the range -90 to 90.\n validate_longitude (float): Ensures `longitude` is within the range -180 to 180.",
"description": "General Prediction Configuration.\n\nThis class provides configuration for prediction settings, allowing users to specify\nparameters such as the forecast duration (in hours) and location (latitude and longitude).\nValidators ensure each parameter is within a specified range. A computed property, `timezone`,\ndetermines the time zone based on latitude and longitude.\n\nAttributes:\n hours (Optional[int]): Number of hours into the future for predictions.\n Must be non-negative.\n historic_hours (Optional[int]): Number of hours into the past for historical data.\n Must be non-negative.\n latitude (Optional[float]): Latitude in degrees, must be between -90 and 90.\n longitude (Optional[float]): Longitude in degrees, must be between -180 and 180.\n\nProperties:\n timezone (Optional[str]): Computed time zone string based on the specified latitude\n and longitude.\n\nValidators:\n validate_hours (int): Ensures `hours` is a non-negative integer.\n validate_historic_hours (int): Ensures `historic_hours` is a non-negative integer.\n validate_latitude (float): Ensures `latitude` is within the range -90 to 90.\n validate_longitude (float): Ensures `longitude` is within the range -180 to 180.",
"properties": {
"historic_hours": {
"anyOf": [
{
"minimum": 0.0,
"type": "integer"
},
{
"type": "null"
}
],
"default": 48,
"description": "Number of hours into the past for historical predictions data",
"title": "Historic Hours"
},
"hours": {
"anyOf": [
{
"minimum": 0.0,
"type": "integer"
},
{
"type": "null"
}
],
"default": 48,
"description": "Number of hours into the future for predictions",
"title": "Hours"
},
"latitude": {
"anyOf": [
{
@ -4682,34 +4710,6 @@
"description": "Longitude in decimal degrees, within -180 to 180 (\u00b0)",
"title": "Longitude"
},
"prediction_historic_hours": {
"anyOf": [
{
"minimum": 0.0,
"type": "integer"
},
{
"type": "null"
}
],
"default": 48,
"description": "Number of hours into the past for historical predictions data",
"title": "Prediction Historic Hours"
},
"prediction_hours": {
"anyOf": [
{
"minimum": 0.0,
"type": "integer"
},
{
"type": "null"
}
],
"default": 48,
"description": "Number of hours into the future for predictions",
"title": "Prediction Hours"
},
"timezone": {
"anyOf": [
{
@ -4838,60 +4838,7 @@
"ServerCommonSettings": {
"description": "Server Configuration.\n\nAttributes:\n To be added",
"properties": {
"server_eos_host": {
"anyOf": [
{
"format": "ipvanyaddress",
"type": "string"
},
{
"type": "null"
}
],
"default": "0.0.0.0",
"description": "EOS server IP address.",
"title": "Server Eos Host"
},
"server_eos_port": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": 8503,
"description": "EOS server IP port number.",
"title": "Server Eos Port"
},
"server_eos_startup_eosdash": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"default": true,
"description": "EOS server to start EOSdash server.",
"title": "Server Eos Startup Eosdash"
},
"server_eos_verbose": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"default": false,
"description": "Enable debug output",
"title": "Server Eos Verbose"
},
"server_eosdash_host": {
"eosdash_host": {
"anyOf": [
{
"format": "ipvanyaddress",
@ -4903,9 +4850,9 @@
],
"default": "0.0.0.0",
"description": "EOSdash server IP address.",
"title": "Server Eosdash Host"
"title": "Eosdash Host"
},
"server_eosdash_port": {
"eosdash_port": {
"anyOf": [
{
"type": "integer"
@ -4916,7 +4863,60 @@
],
"default": 8504,
"description": "EOSdash server IP port number.",
"title": "Server Eosdash Port"
"title": "Eosdash Port"
},
"host": {
"anyOf": [
{
"format": "ipvanyaddress",
"type": "string"
},
{
"type": "null"
}
],
"default": "0.0.0.0",
"description": "EOS server IP address.",
"title": "Host"
},
"port": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": 8503,
"description": "EOS server IP port number.",
"title": "Port"
},
"startup_eosdash": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"default": true,
"description": "EOS server to start EOSdash server.",
"title": "Startup Eosdash"
},
"verbose": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"default": false,
"description": "Enable debug output",
"title": "Verbose"
}
},
"title": "ServerCommonSettings",
@ -5390,6 +5390,21 @@
"WeatherCommonSettings": {
"description": "Weather Forecast Configuration.",
"properties": {
"provider": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Weather provider id of provider to be used.",
"examples": [
"WeatherImport"
],
"title": "Provider"
},
"provider_settings": {
"anyOf": [
{
@ -5403,21 +5418,6 @@
"examples": [
null
]
},
"weather_provider": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Weather provider id of provider to be used.",
"examples": [
"WeatherImport"
],
"title": "Weather Provider"
}
},
"title": "WeatherCommonSettings",
@ -5426,7 +5426,7 @@
"WeatherImportCommonSettings": {
"description": "Common settings for weather data import from file or JSON string.",
"properties": {
"weatherimport_file_path": {
"import_file_path": {
"anyOf": [
{
"type": "string"
@ -5444,9 +5444,9 @@
null,
"/path/to/weather_data.json"
],
"title": "Weatherimport File Path"
"title": "Import File Path"
},
"weatherimport_json": {
"import_json": {
"anyOf": [
{
"type": "string"
@ -5459,7 +5459,7 @@
"examples": [
"{\"weather_temp_air\": [18.3, 17.8, 16.9]}"
],
"title": "Weatherimport Json"
"title": "Import Json"
}
},
"title": "WeatherImportCommonSettings",
@ -5519,7 +5519,7 @@
},
"/gesamtlast_simple": {
"get": {
"description": "Deprecated: Total Load Prediction.\n\nEndpoint to handle total load prediction.\n\nTotal load prediction starts at 00.00.00 today and is provided for 48 hours.\nIf no prediction values are available the missing ones at the start of the series are\nfilled with the first available prediction value.\n\nArgs:\n year_energy (float): Yearly energy consumption in Wh.\n\nNote:\n Set LoadAkkudoktor as load_provider, then update data with\n '/v1/prediction/update'\n and then request data with\n '/v1/prediction/list?key=load_mean' instead.",
"description": "Deprecated: Total Load Prediction.\n\nEndpoint to handle total load prediction.\n\nTotal load prediction starts at 00.00.00 today and is provided for 48 hours.\nIf no prediction values are available the missing ones at the start of the series are\nfilled with the first available prediction value.\n\nArgs:\n year_energy (float): Yearly energy consumption in Wh.\n\nNote:\n Set LoadAkkudoktor as provider, then update data with\n '/v1/prediction/update'\n and then request data with\n '/v1/prediction/list?key=load_mean' instead.",
"operationId": "fastapi_gesamtlast_simple_gesamtlast_simple_get",
"parameters": [
{
@ -5621,7 +5621,7 @@
},
"/pvforecast": {
"get": {
"description": "Deprecated: PV Forecast Prediction.\n\nEndpoint to handle PV forecast prediction.\n\nPVForecast starts at 00.00.00 today and is provided for 48 hours.\nIf no forecast values are available the missing ones at the start of the series are\nfilled with the first available forecast value.\n\nNote:\n Set PVForecastAkkudoktor as pvforecast_provider, then update data with\n '/v1/prediction/update'\n and then request data with\n '/v1/prediction/list?key=pvforecast_ac_power' and\n '/v1/prediction/list?key=pvforecastakkudoktor_temp_air' instead.",
"description": "Deprecated: PV Forecast Prediction.\n\nEndpoint to handle PV forecast prediction.\n\nPVForecast starts at 00.00.00 today and is provided for 48 hours.\nIf no forecast values are available the missing ones at the start of the series are\nfilled with the first available forecast value.\n\nNote:\n Set PVForecastAkkudoktor as provider, then update data with\n '/v1/prediction/update'\n and then request data with\n '/v1/prediction/list?key=pvforecast_ac_power' and\n '/v1/prediction/list?key=pvforecastakkudoktor_temp_air' instead.",
"operationId": "fastapi_pvforecast_pvforecast_get",
"responses": {
"200": {
@ -5640,7 +5640,7 @@
},
"/strompreis": {
"get": {
"description": "Deprecated: Electricity Market Price Prediction per Wh (\u20ac/Wh).\n\nElectricity prices start at 00.00.00 today and are provided for 48 hours.\nIf no prices are available the missing ones at the start of the series are\nfilled with the first available price.\n\nNote:\n Electricity price charges are added.\n\nNote:\n Set ElecPriceAkkudoktor as elecprice_provider, then update data with\n '/v1/prediction/update'\n and then request data with\n '/v1/prediction/list?key=elecprice_marketprice_wh' or\n '/v1/prediction/list?key=elecprice_marketprice_kwh' instead.",
"description": "Deprecated: Electricity Market Price Prediction per Wh (\u20ac/Wh).\n\nElectricity prices start at 00.00.00 today and are provided for 48 hours.\nIf no prices are available the missing ones at the start of the series are\nfilled with the first available price.\n\nNote:\n Electricity price charges are added.\n\nNote:\n Set ElecPriceAkkudoktor as provider, then update data with\n '/v1/prediction/update'\n and then request data with\n '/v1/prediction/list?key=elecprice_marketprice_wh' or\n '/v1/prediction/list?key=elecprice_marketprice_kwh' instead.",
"operationId": "fastapi_strompreis_strompreis_get",
"responses": {
"200": {

View File

@ -31,14 +31,14 @@ def prepare_optimization_real_parameters() -> OptimizationParameters:
# Make a config
settings = {
"prediction": {
"prediction_hours": 48,
"prediction_historic_hours": 24,
"hours": 48,
"historic_hours": 24,
"latitude": 52.52,
"longitude": 13.405,
},
# PV Forecast
"pvforecast": {
"pvforecast_provider": "PVForecastAkkudoktor",
"provider": "PVForecastAkkudoktor",
"pvforecast0_peakpower": 5.0,
"pvforecast0_surface_azimuth": -10,
"pvforecast0_surface_tilt": 7,
@ -63,15 +63,15 @@ def prepare_optimization_real_parameters() -> OptimizationParameters:
},
# Weather Forecast
"weather": {
"weather_provider": "ClearOutside",
"provider": "ClearOutside",
},
# Electricity Price Forecast
"elecprice": {
"elecprice_provider": "ElecPriceAkkudoktor",
"provider": "Akkudoktor",
},
# Load Forecast
"load": {
"load_provider": "LoadAkkudoktor",
"provider": "LoadAkkudoktor",
"provider_settings": {
"loadakkudoktor_year_energy": 5000, # Energy consumption per year in kWh
},
@ -144,7 +144,7 @@ def prepare_optimization_real_parameters() -> OptimizationParameters:
"initial_soc_percentage": 15,
"min_soc_percentage": 15,
},
"inverter": {"device_id": "iv1", "max_power_wh": 10000, "battery": "battery1"},
"inverter": {"device_id": "iv1", "max_power_wh": 10000, "battery_id": "battery1"},
"eauto": {
"device_id": "ev1",
"min_soc_percentage": 50,
@ -341,7 +341,7 @@ def run_optimization(
# Initialize the optimization problem using the default configuration
config_eos = get_config()
config_eos.merge_settings_from_dict(
{"prediction": {"prediction_hours": 48}, "optimization": {"optimization_hours": 48}}
{"prediction": {"hours": 48}, "optimization": {"hours": 48}}
)
opt_class = optimization_problem(verbose=verbose, fixed_seed=seed)

View File

@ -17,13 +17,13 @@ def config_pvforecast() -> dict:
"""Configure settings for PV forecast."""
settings = {
"prediction": {
"prediction_hours": 48,
"prediction_historic_hours": 24,
"hours": 48,
"historic_hours": 24,
"latitude": 52.52,
"longitude": 13.405,
},
"pvforecast": {
"pvforecast_provider": "PVForecastAkkudoktor",
"provider": "PVForecastAkkudoktor",
"pvforecast0_peakpower": 5.0,
"pvforecast0_surface_azimuth": -10,
"pvforecast0_surface_tilt": 7,
@ -54,8 +54,8 @@ def config_weather() -> dict:
"""Configure settings for weather forecast."""
settings = {
"prediction": {
"prediction_hours": 48,
"prediction_historic_hours": 24,
"hours": 48,
"historic_hours": 24,
"latitude": 52.52,
"longitude": 13.405,
},
@ -68,8 +68,8 @@ def config_elecprice() -> dict:
"""Configure settings for electricity price forecast."""
settings = {
"prediction": {
"prediction_hours": 48,
"prediction_historic_hours": 24,
"hours": 48,
"historic_hours": 24,
"latitude": 52.52,
"longitude": 13.405,
},
@ -82,8 +82,8 @@ def config_load() -> dict:
"""Configure settings for load forecast."""
settings = {
"prediction": {
"prediction_hours": 48,
"prediction_historic_hours": 24,
"hours": 48,
"historic_hours": 24,
"latitude": 52.52,
"longitude": 13.405,
}
@ -108,17 +108,17 @@ def run_prediction(provider_id: str, verbose: bool = False) -> str:
print(f"\nProvider ID: {provider_id}")
if provider_id in ("PVForecastAkkudoktor",):
settings = config_pvforecast()
settings["pvforecast"]["pvforecast_provider"] = provider_id
settings["pvforecast"]["provider"] = provider_id
elif provider_id in ("BrightSky", "ClearOutside"):
settings = config_weather()
settings["weather"]["weather_provider"] = provider_id
elif provider_id in ("ElecPriceAkkudoktor",):
settings["weather"]["provider"] = provider_id
elif provider_id in ("Akkudoktor",):
settings = config_elecprice()
settings["elecprice"]["elecprice_provider"] = provider_id
settings["elecprice"]["provider"] = provider_id
elif provider_id in ("LoadAkkudoktor",):
settings = config_elecprice()
settings["load"]["loadakkudoktor_year_energy"] = 1000
settings["load"]["load_provider"] = provider_id
settings["load"]["provider"] = provider_id
else:
raise ValueError(f"Unknown provider '{provider_id}'.")
config_eos.merge_settings_from_dict(settings)

View File

@ -179,7 +179,7 @@ class ConfigEOS(SingletonMixin, SettingsEOSDefaults):
To initialize and access configuration attributes (only one instance is created):
```python
config_eos = ConfigEOS() # Always returns the same instance
print(config_eos.prediction.prediction_hours) # Access a setting from the loaded configuration
print(config_eos.prediction.hours) # Access a setting from the loaded configuration
```
"""
@ -328,7 +328,7 @@ class ConfigEOS(SingletonMixin, SettingsEOSDefaults):
Example:
>>> config = get_config()
>>> new_data = {"prediction": {"prediction_hours": 24}, "server": {"server_eos_port": 8000}}
>>> new_data = {"prediction": {"hours": 24}, "server": {"port": 8000}}
>>> config.merge_settings_from_dict(new_data)
"""
self._setup(**merge_models(self, data))

View File

@ -198,9 +198,9 @@ class EnergieManagementSystem(SingletonMixin, ConfigMixin, PredictionMixin, Pyda
self.ev = ev
self.home_appliance = home_appliance
self.inverter = inverter
self.ac_charge_hours = np.full(self.config.prediction.prediction_hours, 0.0)
self.dc_charge_hours = np.full(self.config.prediction.prediction_hours, 1.0)
self.ev_charge_hours = np.full(self.config.prediction.prediction_hours, 0.0)
self.ac_charge_hours = np.full(self.config.prediction.hours, 0.0)
self.dc_charge_hours = np.full(self.config.prediction.hours, 1.0)
self.ev_charge_hours = np.full(self.config.prediction.hours, 0.0)
def set_akku_discharge_hours(self, ds: np.ndarray) -> None:
if self.battery is not None:
@ -251,7 +251,7 @@ class EnergieManagementSystem(SingletonMixin, ConfigMixin, PredictionMixin, Pyda
error_msg = "Start datetime unknown."
logger.error(error_msg)
raise ValueError(error_msg)
if self.config.prediction.prediction_hours is None:
if self.config.prediction.hours is None:
error_msg = "Prediction hours unknown."
logger.error(error_msg)
raise ValueError(error_msg)

View File

@ -4,7 +4,6 @@ Kept in an extra module to avoid cyclic dependencies on package import.
"""
import logging
import os
from typing import Optional
from pydantic import Field, computed_field, field_validator
@ -16,21 +15,18 @@ from akkudoktoreos.core.logabc import logging_str_to_level
class LoggingCommonSettings(SettingsBaseModel):
"""Logging Configuration."""
logging_level_default: Optional[str] = Field(
level: Optional[str] = Field(
default=None,
description="EOS default logging level.",
examples=["INFO", "DEBUG", "WARNING", "ERROR", "CRITICAL"],
)
# Validators
@field_validator("logging_level_default", mode="after")
@field_validator("level", mode="after")
@classmethod
def set_default_logging_level(cls, value: Optional[str]) -> Optional[str]:
if isinstance(value, str) and value.upper() == "NONE":
value = None
if value is None and (env_level := os.getenv("EOS_LOGGING_LEVEL")) is not None:
# Take default logging level from special environment variable
value = env_level
if value is None:
return None
level = logging_str_to_level(value)
@ -40,7 +36,7 @@ class LoggingCommonSettings(SettingsBaseModel):
# Computed fields
@computed_field # type: ignore[prop-decorator]
@property
def logging_level_root(self) -> str:
def root_level(self) -> str:
"""Root logger logging level."""
level = logging.getLogger().getEffectiveLevel()
level_name = logging.getLevelName(level)

View File

@ -51,16 +51,16 @@ class DevicesStartEndMixin(ConfigMixin, EnergyManagementSystemMixin):
@computed_field # type: ignore[prop-decorator]
@property
def end_datetime(self) -> Optional[DateTime]:
"""Compute the end datetime based on the `start_datetime` and `prediction_hours`.
"""Compute the end datetime based on the `start_datetime` and `hours`.
Ajusts the calculated end time if DST transitions occur within the prediction window.
Returns:
Optional[DateTime]: The calculated end datetime, or `None` if inputs are missing.
"""
if self.ems.start_datetime and self.config.prediction.prediction_hours:
if self.ems.start_datetime and self.config.prediction.hours:
end_datetime = self.ems.start_datetime + to_duration(
f"{self.config.prediction.prediction_hours} hours"
f"{self.config.prediction.hours} hours"
)
dst_change = end_datetime.offset_hours - self.ems.start_datetime.offset_hours
logger.debug(

View File

@ -18,9 +18,9 @@ class Heatpump:
COP_COEFFICIENT = 0.1
"""COP increase per degree"""
def __init__(self, max_heat_output: int, prediction_hours: int):
def __init__(self, max_heat_output: int, hours: int):
self.max_heat_output = max_heat_output
self.prediction_hours = prediction_hours
self.hours = hours
self.log = logging.getLogger(__name__)
def __check_outside_temperature_range__(self, temp_celsius: float) -> bool:
@ -117,9 +117,9 @@ class Heatpump:
"""Simulate power data for 24 hours based on provided temperatures."""
power_data: List[float] = []
if len(temperatures) != self.prediction_hours:
if len(temperatures) != self.hours:
raise ValueError(
f"The temperature array must contain exactly {self.prediction_hours} entries, "
f"The temperature array must contain exactly {self.hours} entries, "
"one for each hour of the day."
)

View File

@ -14,7 +14,7 @@ class InverterParameters(DeviceParameters):
device_id: str = Field(description="ID of inverter", examples=["inverter1"])
max_power_wh: float = Field(gt=0, examples=[10000])
battery: Optional[str] = Field(
battery_id: Optional[str] = Field(
default=None, description="ID of battery", examples=[None, "battery1"]
)
@ -29,7 +29,7 @@ class Inverter(DeviceBase):
def _setup(self) -> None:
assert self.parameters is not None
if self.parameters.battery is None:
if self.parameters.battery_id is None:
# For the moment raise exception
# TODO: Make battery configurable by config
error_msg = "Battery for PV inverter is mandatory."
@ -42,7 +42,7 @@ class Inverter(DeviceBase):
def _post_setup(self) -> None:
assert self.parameters is not None
self.battery = self.devices.get_device_by_id(self.parameters.battery)
self.battery = self.devices.get_device_by_id(self.parameters.battery_id)
def process_energy(
self, generation: float, consumption: float, hour: int

View File

@ -25,19 +25,19 @@ logger = get_logger(__name__)
class MeasurementCommonSettings(SettingsBaseModel):
"""Measurement Configuration."""
measurement_load0_name: Optional[str] = Field(
load0_name: Optional[str] = Field(
default=None, description="Name of the load0 source", examples=["Household", "Heat Pump"]
)
measurement_load1_name: Optional[str] = Field(
load1_name: Optional[str] = Field(
default=None, description="Name of the load1 source", examples=[None]
)
measurement_load2_name: Optional[str] = Field(
load2_name: Optional[str] = Field(
default=None, description="Name of the load2 source", examples=[None]
)
measurement_load3_name: Optional[str] = Field(
load3_name: Optional[str] = Field(
default=None, description="Name of the load3 source", examples=[None]
)
measurement_load4_name: Optional[str] = Field(
load4_name: Optional[str] = Field(
default=None, description="Name of the load4 source", examples=[None]
)
@ -50,42 +50,42 @@ class MeasurementDataRecord(DataRecord):
"""
# Single loads, to be aggregated to total load
measurement_load0_mr: Optional[float] = Field(
load0_mr: Optional[float] = Field(
default=None, ge=0, description="Load0 meter reading [kWh]", examples=[40421]
)
measurement_load1_mr: Optional[float] = Field(
load1_mr: Optional[float] = Field(
default=None, ge=0, description="Load1 meter reading [kWh]", examples=[None]
)
measurement_load2_mr: Optional[float] = Field(
load2_mr: Optional[float] = Field(
default=None, ge=0, description="Load2 meter reading [kWh]", examples=[None]
)
measurement_load3_mr: Optional[float] = Field(
load3_mr: Optional[float] = Field(
default=None, ge=0, description="Load3 meter reading [kWh]", examples=[None]
)
measurement_load4_mr: Optional[float] = Field(
load4_mr: Optional[float] = Field(
default=None, ge=0, description="Load4 meter reading [kWh]", examples=[None]
)
measurement_max_loads: ClassVar[int] = 5 # Maximum number of loads that can be set
max_loads: ClassVar[int] = 5 # Maximum number of loads that can be set
measurement_grid_export_mr: Optional[float] = Field(
grid_export_mr: Optional[float] = Field(
default=None, ge=0, description="Export to grid meter reading [kWh]", examples=[1000]
)
measurement_grid_import_mr: Optional[float] = Field(
grid_import_mr: Optional[float] = Field(
default=None, ge=0, description="Import from grid meter reading [kWh]", examples=[1000]
)
# Computed fields
@computed_field # type: ignore[prop-decorator]
@property
def measurement_loads(self) -> List[str]:
def loads(self) -> List[str]:
"""Compute a list of active loads."""
active_loads = []
# Loop through measurement_loadx
for i in range(self.measurement_max_loads):
load_attr = f"measurement_load{i}_mr"
# Loop through loadx
for i in range(self.max_loads):
load_attr = f"load{i}_mr"
# Check if either attribute is set and add to active loads
if getattr(self, load_attr, None):
@ -105,7 +105,7 @@ class Measurement(SingletonMixin, DataImportMixin, DataSequence):
)
topics: ClassVar[List[str]] = [
"measurement_load",
"load",
]
def __init__(self, *args: Any, **kwargs: Any) -> None:
@ -147,14 +147,16 @@ class Measurement(SingletonMixin, DataImportMixin, DataSequence):
"""Provides measurement key for given name and topic."""
topic = topic.lower()
print(self.topics)
if topic not in self.topics:
return None
topic_keys = [
key for key in self.config.measurement.model_fields.keys() if key.startswith(topic)
]
print(topic_keys)
key = None
if topic == "measurement_load":
if topic == "load":
for config_key in topic_keys:
if (
config_key.endswith("_name")
@ -255,9 +257,9 @@ class Measurement(SingletonMixin, DataImportMixin, DataSequence):
end_datetime = self[-1].date_time
size = self._interval_count(start_datetime, end_datetime, interval)
load_total_array = np.zeros(size)
# Loop through measurement_load<x>_mr
for i in range(self.record_class().measurement_max_loads):
key = f"measurement_load{i}_mr"
# Loop through load<x>_mr
for i in range(self.record_class().max_loads):
key = f"load{i}_mr"
# Calculate load per interval
load_array = self._energy_from_meter_readings(
key=key, start_datetime=start_datetime, end_datetime=end_datetime, interval=interval

View File

@ -110,12 +110,8 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
):
"""Initialize the optimization problem with the required parameters."""
self.opti_param: dict[str, Any] = {}
self.fixed_eauto_hours = (
self.config.prediction.prediction_hours - self.config.optimization.optimization_hours
)
self.possible_charge_values = (
self.config.optimization.optimization_ev_available_charge_rates_percent
)
self.fixed_eauto_hours = self.config.prediction.hours - self.config.optimization.hours
self.possible_charge_values = self.config.optimization.ev_available_charge_rates_percent
self.verbose = verbose
self.fix_seed = fixed_seed
self.optimize_ev = True
@ -182,27 +178,25 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
total_states = 3 * len_ac
# 1. Mutating the charge_discharge part
charge_discharge_part = individual[: self.config.prediction.prediction_hours]
charge_discharge_part = individual[: self.config.prediction.hours]
(charge_discharge_mutated,) = self.toolbox.mutate_charge_discharge(charge_discharge_part)
# Instead of a fixed clamping to 0..8 or 0..6 dynamically:
charge_discharge_mutated = np.clip(charge_discharge_mutated, 0, total_states - 1)
individual[: self.config.prediction.prediction_hours] = charge_discharge_mutated
individual[: self.config.prediction.hours] = charge_discharge_mutated
# 2. Mutating the EV charge part, if active
if self.optimize_ev:
ev_charge_part = individual[
self.config.prediction.prediction_hours : self.config.prediction.prediction_hours
* 2
self.config.prediction.hours : self.config.prediction.hours * 2
]
(ev_charge_part_mutated,) = self.toolbox.mutate_ev_charge_index(ev_charge_part)
ev_charge_part_mutated[
self.config.prediction.prediction_hours - self.fixed_eauto_hours :
] = [0] * self.fixed_eauto_hours
individual[
self.config.prediction.prediction_hours : self.config.prediction.prediction_hours
* 2
] = ev_charge_part_mutated
ev_charge_part_mutated[self.config.prediction.hours - self.fixed_eauto_hours :] = [
0
] * self.fixed_eauto_hours
individual[self.config.prediction.hours : self.config.prediction.hours * 2] = (
ev_charge_part_mutated
)
# 3. Mutating the appliance start time, if applicable
if self.opti_param["home_appliance"] > 0:
@ -216,15 +210,13 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
def create_individual(self) -> list[int]:
# Start with discharge states for the individual
individual_components = [
self.toolbox.attr_discharge_state()
for _ in range(self.config.prediction.prediction_hours)
self.toolbox.attr_discharge_state() for _ in range(self.config.prediction.hours)
]
# Add EV charge index values if optimize_ev is True
if self.optimize_ev:
individual_components += [
self.toolbox.attr_ev_charge_index()
for _ in range(self.config.prediction.prediction_hours)
self.toolbox.attr_ev_charge_index() for _ in range(self.config.prediction.hours)
]
# Add the start time of the household appliance if it's being optimized
@ -257,7 +249,7 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
individual.extend(eautocharge_hours_index.tolist())
elif self.optimize_ev:
# Falls optimize_ev aktiv ist, aber keine EV-Daten vorhanden sind, fügen wir Nullen hinzu
individual.extend([0] * self.config.prediction.prediction_hours)
individual.extend([0] * self.config.prediction.hours)
# Add dishwasher start time if applicable
if self.opti_param.get("home_appliance", 0) > 0 and washingstart_int is not None:
@ -279,17 +271,12 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
3. Dishwasher start time (integer if applicable).
"""
# Discharge hours as a NumPy array of ints
discharge_hours_bin = np.array(
individual[: self.config.prediction.prediction_hours], dtype=int
)
discharge_hours_bin = np.array(individual[: self.config.prediction.hours], dtype=int)
# EV charge hours as a NumPy array of ints (if optimize_ev is True)
eautocharge_hours_index = (
np.array(
individual[
self.config.prediction.prediction_hours : self.config.prediction.prediction_hours
* 2
],
individual[self.config.prediction.hours : self.config.prediction.hours * 2],
dtype=int,
)
if self.optimize_ev
@ -401,7 +388,7 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
)
self.ems.set_ev_charge_hours(eautocharge_hours_float)
else:
self.ems.set_ev_charge_hours(np.full(self.config.prediction.prediction_hours, 0))
self.ems.set_ev_charge_hours(np.full(self.config.prediction.hours, 0))
return self.ems.simulate(self.ems.start_datetime.hour)
@ -463,7 +450,7 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
# min_length = min(battery_soc_per_hour.size, discharge_hours_bin.size)
# battery_soc_per_hour_tail = battery_soc_per_hour[-min_length:]
# discharge_hours_bin_tail = discharge_hours_bin[-min_length:]
# len_ac = len(self.config.optimization.optimization_ev_available_charge_rates_percent)
# len_ac = len(self.config.optimization.ev_available_charge_rates_percent)
# # # Find hours where battery SoC is 0
# # zero_soc_mask = battery_soc_per_hour_tail == 0
@ -512,7 +499,7 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
if parameters.eauto and self.ems.ev
else 0
)
* self.config.optimization.optimization_penalty,
* self.config.optimization.penalty,
)
return (gesamtbilanz,)
@ -580,7 +567,7 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
start_hour = self.ems.start_datetime.hour
einspeiseverguetung_euro_pro_wh = np.full(
self.config.prediction.prediction_hours, parameters.ems.einspeiseverguetung_euro_pro_wh
self.config.prediction.hours, parameters.ems.einspeiseverguetung_euro_pro_wh
)
# TODO: Refactor device setup phase out
@ -591,7 +578,7 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
if parameters.pv_akku:
akku = Battery(parameters.pv_akku)
self.devices.add_device(akku)
akku.set_charge_per_hour(np.full(self.config.prediction.prediction_hours, 1))
akku.set_charge_per_hour(np.full(self.config.prediction.hours, 1))
eauto: Optional[Battery] = None
if parameters.eauto:
@ -599,7 +586,7 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
parameters.eauto,
)
self.devices.add_device(eauto)
eauto.set_charge_per_hour(np.full(self.config.prediction.prediction_hours, 1))
eauto.set_charge_per_hour(np.full(self.config.prediction.hours, 1))
self.optimize_ev = (
parameters.eauto.min_soc_percentage - parameters.eauto.initial_soc_percentage >= 0
)

View File

@ -12,18 +12,16 @@ class OptimizationCommonSettings(SettingsBaseModel):
"""General Optimization Configuration.
Attributes:
optimization_hours (int): Number of hours for optimizations.
hours (int): Number of hours for optimizations.
"""
optimization_hours: Optional[int] = Field(
hours: Optional[int] = Field(
default=48, ge=0, description="Number of hours into the future for optimizations."
)
optimization_penalty: Optional[int] = Field(
default=10, description="Penalty factor used in optimization."
)
penalty: Optional[int] = Field(default=10, description="Penalty factor used in optimization.")
optimization_ev_available_charge_rates_percent: Optional[List[float]] = Field(
ev_available_charge_rates_percent: Optional[List[float]] = Field(
default=[
0.0,
6.0 / 16.0,

View File

@ -9,12 +9,12 @@ from akkudoktoreos.prediction.elecpriceimport import ElecPriceImportCommonSettin
class ElecPriceCommonSettings(SettingsBaseModel):
"""Electricity Price Prediction Configuration."""
elecprice_provider: Optional[str] = Field(
provider: Optional[str] = Field(
default=None,
description="Electricity price provider id of provider to be used.",
examples=["ElecPriceAkkudoktor"],
)
elecprice_charges_kwh: Optional[float] = Field(
charges_kwh: Optional[float] = Field(
default=None, ge=0, description="Electricity price charges (€/kWh).", examples=[0.21]
)

View File

@ -49,15 +49,15 @@ class ElecPriceProvider(PredictionProvider):
electricity price_provider (str): Prediction provider for electricity price.
Attributes:
prediction_hours (int, optional): The number of hours into the future for which predictions are generated.
prediction_historic_hours (int, optional): The number of past hours for which historical data is retained.
hours (int, optional): The number of hours into the future for which predictions are generated.
historic_hours (int, optional): The number of past hours for which historical data is retained.
latitude (float, optional): The latitude in degrees, must be within -90 to 90.
longitude (float, optional): The longitude in degrees, must be within -180 to 180.
start_datetime (datetime, optional): The starting datetime for predictions, defaults to the current datetime if unspecified.
end_datetime (datetime, computed): The datetime representing the end of the prediction range,
calculated based on `start_datetime` and `prediction_hours`.
calculated based on `start_datetime` and `hours`.
keep_datetime (datetime, computed): The earliest datetime for retaining historical data, calculated
based on `start_datetime` and `prediction_historic_hours`.
based on `start_datetime` and `historic_hours`.
"""
# overload
@ -71,4 +71,4 @@ class ElecPriceProvider(PredictionProvider):
return "ElecPriceProvider"
def enabled(self) -> bool:
return self.provider_id() == self.config.elecprice.elecprice_provider
return self.provider_id() == self.config.elecprice.provider

View File

@ -54,11 +54,11 @@ class ElecPriceAkkudoktor(ElecPriceProvider):
of hours into the future and retains historical data.
Attributes:
prediction_hours (int, optional): Number of hours in the future for the forecast.
prediction_historic_hours (int, optional): Number of past hours for retaining data.
hours (int, optional): Number of hours in the future for the forecast.
historic_hours (int, optional): Number of past hours for retaining data.
start_datetime (datetime, optional): Start datetime for forecasts, defaults to the current datetime.
end_datetime (datetime, computed): The forecast's end datetime, computed based on `start_datetime` and `prediction_hours`.
keep_datetime (datetime, computed): The datetime to retain historical data, computed from `start_datetime` and `prediction_historic_hours`.
end_datetime (datetime, computed): The forecast's end datetime, computed based on `start_datetime` and `hours`.
keep_datetime (datetime, computed): The datetime to retain historical data, computed from `start_datetime` and `historic_hours`.
Methods:
provider_id(): Returns a unique identifier for the provider.
@ -125,18 +125,16 @@ class ElecPriceAkkudoktor(ElecPriceProvider):
capped_data = data.clip(min=lower_bound, max=upper_bound)
return capped_data
def _predict_ets(
self, history: np.ndarray, seasonal_periods: int, prediction_hours: int
) -> np.ndarray:
def _predict_ets(self, history: np.ndarray, seasonal_periods: int, hours: int) -> np.ndarray:
clean_history = self._cap_outliers(history)
model = ExponentialSmoothing(
clean_history, seasonal="add", seasonal_periods=seasonal_periods
).fit()
return model.forecast(prediction_hours)
return model.forecast(hours)
def _predict_median(self, history: np.ndarray, prediction_hours: int) -> np.ndarray:
def _predict_median(self, history: np.ndarray, hours: int) -> np.ndarray:
clean_history = self._cap_outliers(history)
return np.full(prediction_hours, np.median(clean_history))
return np.full(hours, np.median(clean_history))
def _update_data(
self, force_update: Optional[bool] = False
@ -155,8 +153,8 @@ class ElecPriceAkkudoktor(ElecPriceProvider):
# Assumption that all lists are the same length and are ordered chronologically
# in ascending order and have the same timestamps.
# Get elecprice_charges_kwh in wh
charges_wh = (self.config.elecprice.elecprice_charges_kwh or 0) / 1000
# Get charges_kwh in wh
charges_wh = (self.config.elecprice.charges_kwh or 0) / 1000
highest_orig_datetime = None # newest datetime from the api after that we want to update.
series_data = pd.Series(dtype=float) # Initialize an empty series
@ -183,27 +181,23 @@ class ElecPriceAkkudoktor(ElecPriceProvider):
assert highest_orig_datetime # mypy fix
# some of our data is already in the future, so we need to predict less. If we got less data we increase the prediction hours
needed_prediction_hours = int(
self.config.prediction.prediction_hours
needed_hours = int(
self.config.prediction.hours
- ((highest_orig_datetime - self.start_datetime).total_seconds() // 3600)
)
if needed_prediction_hours <= 0:
if needed_hours <= 0:
logger.warning(
f"No prediction needed. needed_prediction_hours={needed_prediction_hours}, prediction_hours={self.config.prediction.prediction_hours},highest_orig_datetime {highest_orig_datetime}, start_datetime {self.start_datetime}"
) # this might keep data longer than self.start_datetime + self.config.prediction.prediction_hours in the records
f"No prediction needed. needed_hours={needed_hours}, hours={self.config.prediction.hours},highest_orig_datetime {highest_orig_datetime}, start_datetime {self.start_datetime}"
) # this might keep data longer than self.start_datetime + self.config.prediction.hours in the records
return
if amount_datasets > 800: # we do the full ets with seasons of 1 week
prediction = self._predict_ets(
history, seasonal_periods=168, prediction_hours=needed_prediction_hours
)
prediction = self._predict_ets(history, seasonal_periods=168, hours=needed_hours)
elif amount_datasets > 168: # not enough data to do seasons of 1 week, but enough for 1 day
prediction = self._predict_ets(
history, seasonal_periods=24, prediction_hours=needed_prediction_hours
)
prediction = self._predict_ets(history, seasonal_periods=24, hours=needed_hours)
elif amount_datasets > 0: # not enough data for ets, do median
prediction = self._predict_median(history, prediction_hours=needed_prediction_hours)
prediction = self._predict_median(history, hours=needed_hours)
else:
logger.error("No data available for prediction")
raise ValueError("No data available")

View File

@ -22,24 +22,22 @@ logger = get_logger(__name__)
class ElecPriceImportCommonSettings(SettingsBaseModel):
"""Common settings for elecprice data import from file or JSON String."""
elecpriceimport_file_path: Optional[Union[str, Path]] = Field(
import_file_path: Optional[Union[str, Path]] = Field(
default=None,
description="Path to the file to import elecprice data from.",
examples=[None, "/path/to/prices.json"],
)
elecpriceimport_json: Optional[str] = Field(
import_json: Optional[str] = Field(
default=None,
description="JSON string, dictionary of electricity price forecast value lists.",
examples=['{"elecprice_marketprice_wh": [0.0003384, 0.0003318, 0.0003284]}'],
)
# Validators
@field_validator("elecpriceimport_file_path", mode="after")
@field_validator("import_file_path", mode="after")
@classmethod
def validate_elecpriceimport_file_path(
cls, value: Optional[Union[str, Path]]
) -> Optional[Path]:
def validate_import_file_path(cls, value: Optional[Union[str, Path]]) -> Optional[Path]:
if value is None:
return None
if isinstance(value, str):
@ -65,12 +63,12 @@ class ElecPriceImport(ElecPriceProvider, PredictionImportProvider):
return "ElecPriceImport"
def _update_data(self, force_update: Optional[bool] = False) -> None:
if self.config.elecprice.provider_settings.elecpriceimport_file_path is not None:
if self.config.elecprice.provider_settings.import_file_path is not None:
self.import_from_file(
self.config.elecprice.provider_settings.elecpriceimport_file_path,
self.config.elecprice.provider_settings.import_file_path,
key_prefix="elecprice",
)
if self.config.elecprice.provider_settings.elecpriceimport_json is not None:
if self.config.elecprice.provider_settings.import_json is not None:
self.import_from_json(
self.config.elecprice.provider_settings.elecpriceimport_json, key_prefix="elecprice"
self.config.elecprice.provider_settings.import_json, key_prefix="elecprice"
)

View File

@ -15,7 +15,7 @@ logger = get_logger(__name__)
class LoadCommonSettings(SettingsBaseModel):
"""Load Prediction Configuration."""
load_provider: Optional[str] = Field(
provider: Optional[str] = Field(
default=None,
description="Load provider id of provider to be used.",
examples=["LoadAkkudoktor"],

View File

@ -33,18 +33,18 @@ class LoadProvider(PredictionProvider):
LoadProvider is a thread-safe singleton, ensuring only one instance of this class is created.
Configuration variables:
load_provider (str): Prediction provider for load.
provider (str): Prediction provider for load.
Attributes:
prediction_hours (int, optional): The number of hours into the future for which predictions are generated.
prediction_historic_hours (int, optional): The number of past hours for which historical data is retained.
hours (int, optional): The number of hours into the future for which predictions are generated.
historic_hours (int, optional): The number of past hours for which historical data is retained.
latitude (float, optional): The latitude in degrees, must be within -90 to 90.
longitude (float, optional): The longitude in degrees, must be within -180 to 180.
start_datetime (datetime, optional): The starting datetime for predictions, defaults to the current datetime if unspecified.
end_datetime (datetime, computed): The datetime representing the end of the prediction range,
calculated based on `start_datetime` and `prediction_hours`.
calculated based on `start_datetime` and `hours`.
keep_datetime (datetime, computed): The earliest datetime for retaining historical data, calculated
based on `start_datetime` and `prediction_historic_hours`.
based on `start_datetime` and `historic_hours`.
"""
# overload
@ -58,4 +58,4 @@ class LoadProvider(PredictionProvider):
return "LoadProvider"
def enabled(self) -> bool:
return self.provider_id() == self.config.load.load_provider
return self.provider_id() == self.config.load.provider

View File

@ -111,7 +111,7 @@ class LoadAkkudoktor(LoadProvider):
# We provide prediction starting at start of day, to be compatible to old system.
# End date for prediction is prediction hours from now.
date = self.start_datetime.start_of("day")
end_date = self.start_datetime.add(hours=self.config.prediction.prediction_hours)
end_date = self.start_datetime.add(hours=self.config.prediction.hours)
while compare_datetimes(date, end_date).lt:
# Extract mean (index 0) and standard deviation (index 1) for the given day and hour
# Day indexing starts at 0, -1 because of that

View File

@ -22,19 +22,19 @@ logger = get_logger(__name__)
class LoadImportCommonSettings(SettingsBaseModel):
"""Common settings for load data import from file or JSON string."""
load_import_file_path: Optional[Union[str, Path]] = Field(
import_file_path: Optional[Union[str, Path]] = Field(
default=None,
description="Path to the file to import load data from.",
examples=[None, "/path/to/yearly_load.json"],
)
load_import_json: Optional[str] = Field(
import_json: Optional[str] = Field(
default=None,
description="JSON string, dictionary of load forecast value lists.",
examples=['{"load0_mean": [676.71, 876.19, 527.13]}'],
)
# Validators
@field_validator("load_import_file_path", mode="after")
@field_validator("import_file_path", mode="after")
@classmethod
def validate_loadimport_file_path(cls, value: Optional[Union[str, Path]]) -> Optional[Path]:
if value is None:
@ -62,11 +62,7 @@ class LoadImport(LoadProvider, PredictionImportProvider):
return "LoadImport"
def _update_data(self, force_update: Optional[bool] = False) -> None:
if self.config.load.provider_settings.load_import_file_path is not None:
self.import_from_file(
self.config.provider_settings.load_import_file_path, key_prefix="load"
)
if self.config.load.provider_settings.load_import_json is not None:
self.import_from_json(
self.config.load.provider_settings.load_import_json, key_prefix="load"
)
if self.config.load.provider_settings.import_file_path is not None:
self.import_from_file(self.config.provider_settings.import_file_path, key_prefix="load")
if self.config.load.provider_settings.import_json is not None:
self.import_from_json(self.config.load.provider_settings.import_json, key_prefix="load")

View File

@ -53,9 +53,9 @@ class PredictionCommonSettings(SettingsBaseModel):
determines the time zone based on latitude and longitude.
Attributes:
prediction_hours (Optional[int]): Number of hours into the future for predictions.
hours (Optional[int]): Number of hours into the future for predictions.
Must be non-negative.
prediction_historic_hours (Optional[int]): Number of hours into the past for historical data.
historic_hours (Optional[int]): Number of hours into the past for historical data.
Must be non-negative.
latitude (Optional[float]): Latitude in degrees, must be between -90 and 90.
longitude (Optional[float]): Longitude in degrees, must be between -180 and 180.
@ -65,16 +65,16 @@ class PredictionCommonSettings(SettingsBaseModel):
and longitude.
Validators:
validate_prediction_hours (int): Ensures `prediction_hours` is a non-negative integer.
validate_prediction_historic_hours (int): Ensures `prediction_historic_hours` is a non-negative integer.
validate_hours (int): Ensures `hours` is a non-negative integer.
validate_historic_hours (int): Ensures `historic_hours` is a non-negative integer.
validate_latitude (float): Ensures `latitude` is within the range -90 to 90.
validate_longitude (float): Ensures `longitude` is within the range -180 to 180.
"""
prediction_hours: Optional[int] = Field(
hours: Optional[int] = Field(
default=48, ge=0, description="Number of hours into the future for predictions"
)
prediction_historic_hours: Optional[int] = Field(
historic_hours: Optional[int] = Field(
default=48,
ge=0,
description="Number of hours into the past for historical predictions data",

View File

@ -114,16 +114,16 @@ class PredictionStartEndKeepMixin(PredictionBase):
@computed_field # type: ignore[prop-decorator]
@property
def end_datetime(self) -> Optional[DateTime]:
"""Compute the end datetime based on the `start_datetime` and `prediction_hours`.
"""Compute the end datetime based on the `start_datetime` and `hours`.
Ajusts the calculated end time if DST transitions occur within the prediction window.
Returns:
Optional[DateTime]: The calculated end datetime, or `None` if inputs are missing.
"""
if self.start_datetime and self.config.prediction.prediction_hours:
if self.start_datetime and self.config.prediction.hours:
end_datetime = self.start_datetime + to_duration(
f"{self.config.prediction.prediction_hours} hours"
f"{self.config.prediction.hours} hours"
)
dst_change = end_datetime.offset_hours - self.start_datetime.offset_hours
logger.debug(f"Pre: {self.start_datetime}..{end_datetime}: DST change: {dst_change}")
@ -147,10 +147,10 @@ class PredictionStartEndKeepMixin(PredictionBase):
return None
historic_hours = self.historic_hours_min()
if (
self.config.prediction.prediction_historic_hours
and self.config.prediction.prediction_historic_hours > historic_hours
self.config.prediction.historic_hours
and self.config.prediction.historic_hours > historic_hours
):
historic_hours = int(self.config.prediction.prediction_historic_hours)
historic_hours = int(self.config.prediction.historic_hours)
return self.start_datetime - to_duration(f"{historic_hours} hours")
@computed_field # type: ignore[prop-decorator]

View File

@ -19,7 +19,7 @@ class PVForecastCommonSettings(SettingsBaseModel):
# Inverter Parameters
# https://pvlib-python.readthedocs.io/en/stable/_modules/pvlib/inverter.html
pvforecast_provider: Optional[str] = Field(
provider: Optional[str] = Field(
default=None,
description="PVForecast provider id of provider to be used.",
examples=["PVForecastAkkudoktor"],

View File

@ -28,18 +28,18 @@ class PVForecastProvider(PredictionProvider):
PVForecastProvider is a thread-safe singleton, ensuring only one instance of this class is created.
Configuration variables:
pvforecast_provider (str): Prediction provider for pvforecast.
provider (str): Prediction provider for pvforecast.
Attributes:
prediction_hours (int, optional): The number of hours into the future for which predictions are generated.
prediction_historic_hours (int, optional): The number of past hours for which historical data is retained.
hours (int, optional): The number of hours into the future for which predictions are generated.
historic_hours (int, optional): The number of past hours for which historical data is retained.
latitude (float, optional): The latitude in degrees, must be within -90 to 90.
longitude (float, optional): The longitude in degrees, must be within -180 to 180.
start_datetime (datetime, optional): The starting datetime for predictions (inlcusive), defaults to the current datetime if unspecified.
end_datetime (datetime, computed): The datetime representing the end of the prediction range (exclusive),
calculated based on `start_datetime` and `prediction_hours`.
calculated based on `start_datetime` and `hours`.
keep_datetime (datetime, computed): The earliest datetime for retaining historical data (inclusive), calculated
based on `start_datetime` and `prediction_historic_hours`.
based on `start_datetime` and `historic_hours`.
"""
# overload
@ -54,6 +54,6 @@ class PVForecastProvider(PredictionProvider):
def enabled(self) -> bool:
logger.debug(
f"PVForecastProvider ID {self.provider_id()} vs. config {self.config.pvforecast.pvforecast_provider}"
f"PVForecastProvider ID {self.provider_id()} vs. config {self.config.pvforecast.provider}"
)
return self.provider_id() == self.config.pvforecast.pvforecast_provider
return self.provider_id() == self.config.pvforecast.provider

View File

@ -14,21 +14,25 @@ Classes:
Example:
# Set up the configuration with necessary fields for URL generation
settings_data = {
"prediction_hours": 48,
"prediction_historic_hours": 24,
"latitude": 52.52,
"longitude": 13.405,
"pvforecast_provider": "Akkudoktor",
"pvforecast0_peakpower": 5.0,
"pvforecast0_surface_azimuth": -10,
"pvforecast0_surface_tilt": 7,
"pvforecast0_userhorizon": [20, 27, 22, 20],
"pvforecast0_inverter_paco": 10000,
"pvforecast1_peakpower": 4.8,
"pvforecast1_surface_azimuth": -90,
"pvforecast1_surface_tilt": 7,
"pvforecast1_userhorizon": [30, 30, 30, 50],
"pvforecast1_inverter_paco": 10000,
"prediction": {
"hours": 48,
"historic_hours": 24,
"latitude": 52.52,
"longitude": 13.405,
},
"pvforecast": {
"provider": "PVForecastAkkudoktor",
"pvforecast0_peakpower": 5.0,
"pvforecast0_surface_azimuth": -10,
"pvforecast0_surface_tilt": 7,
"pvforecast0_userhorizon": [20, 27, 22, 20],
"pvforecast0_inverter_paco": 10000,
"pvforecast1_peakpower": 4.8,
"pvforecast1_surface_azimuth": -90,
"pvforecast1_surface_tilt": 7,
"pvforecast1_userhorizon": [30, 30, 30, 50],
"pvforecast1_inverter_paco": 10000,
}
}
# Create the config instance from the provided data
@ -47,12 +51,12 @@ Example:
print(forecast.report_ac_power_and_measurement())
Attributes:
prediction_hours (int): Number of hours into the future to forecast. Default is 48.
prediction_historic_hours (int): Number of past hours to retain for analysis. Default is 24.
hours (int): Number of hours into the future to forecast. Default is 48.
historic_hours (int): Number of past hours to retain for analysis. Default is 24.
latitude (float): Latitude for the forecast location.
longitude (float): Longitude for the forecast location.
start_datetime (datetime): Start time for the forecast, defaulting to current datetime.
end_datetime (datetime): Computed end datetime based on `start_datetime` and `prediction_hours`.
end_datetime (datetime): Computed end datetime based on `start_datetime` and `hours`.
keep_datetime (datetime): Computed threshold datetime for retaining historical data.
Methods:
@ -159,13 +163,13 @@ class PVForecastAkkudoktor(PVForecastProvider):
of hours into the future and retains historical data.
Attributes:
prediction_hours (int, optional): Number of hours in the future for the forecast.
prediction_historic_hours (int, optional): Number of past hours for retaining data.
hours (int, optional): Number of hours in the future for the forecast.
historic_hours (int, optional): Number of past hours for retaining data.
latitude (float, optional): The latitude in degrees, validated to be between -90 and 90.
longitude (float, optional): The longitude in degrees, validated to be between -180 and 180.
start_datetime (datetime, optional): Start datetime for forecasts, defaults to the current datetime.
end_datetime (datetime, computed): The forecast's end datetime, computed based on `start_datetime` and `prediction_hours`.
keep_datetime (datetime, computed): The datetime to retain historical data, computed from `start_datetime` and `prediction_historic_hours`.
end_datetime (datetime, computed): The forecast's end datetime, computed based on `start_datetime` and `hours`.
keep_datetime (datetime, computed): The datetime to retain historical data, computed from `start_datetime` and `historic_hours`.
Methods:
provider_id(): Returns a unique identifier for the provider.
@ -286,10 +290,10 @@ class PVForecastAkkudoktor(PVForecastProvider):
# Assumption that all lists are the same length and are ordered chronologically
# in ascending order and have the same timestamps.
if len(akkudoktor_data.values[0]) < self.config.prediction.prediction_hours:
if len(akkudoktor_data.values[0]) < self.config.prediction.hours:
# Expect one value set per prediction hour
error_msg = (
f"The forecast must cover at least {self.config.prediction.prediction_hours} hours, "
f"The forecast must cover at least {self.config.prediction.hours} hours, "
f"but only {len(akkudoktor_data.values[0])} data sets are given in forecast data."
)
logger.error(f"Akkudoktor schema change: {error_msg}")
@ -318,9 +322,9 @@ class PVForecastAkkudoktor(PVForecastProvider):
self.update_value(dt, data)
if len(self) < self.config.prediction.prediction_hours:
if len(self) < self.config.prediction.hours:
raise ValueError(
f"The forecast must cover at least {self.config.prediction.prediction_hours} hours, "
f"The forecast must cover at least {self.config.prediction.hours} hours, "
f"but only {len(self)} hours starting from {self.start_datetime} "
f"were predicted."
)
@ -370,13 +374,13 @@ if __name__ == "__main__":
# Set up the configuration with necessary fields for URL generation
settings_data = {
"prediction": {
"prediction_hours": 48,
"prediction_historic_hours": 24,
"hours": 48,
"historic_hours": 24,
"latitude": 52.52,
"longitude": 13.405,
},
"pvforecast": {
"pvforecast_provider": "PVForecastAkkudoktor",
"provider": "PVForecastAkkudoktor",
"pvforecast0_peakpower": 5.0,
"pvforecast0_surface_azimuth": -10,
"pvforecast0_surface_tilt": 7,

View File

@ -11,7 +11,7 @@ from akkudoktoreos.prediction.weatherimport import WeatherImportCommonSettings
class WeatherCommonSettings(SettingsBaseModel):
"""Weather Forecast Configuration."""
weather_provider: Optional[str] = Field(
provider: Optional[str] = Field(
default=None,
description="Weather provider id of provider to be used.",
examples=["WeatherImport"],

View File

@ -101,18 +101,18 @@ class WeatherProvider(PredictionProvider):
WeatherProvider is a thread-safe singleton, ensuring only one instance of this class is created.
Configuration variables:
weather_provider (str): Prediction provider for weather.
provider (str): Prediction provider for weather.
Attributes:
prediction_hours (int, optional): The number of hours into the future for which predictions are generated.
prediction_historic_hours (int, optional): The number of past hours for which historical data is retained.
hours (int, optional): The number of hours into the future for which predictions are generated.
historic_hours (int, optional): The number of past hours for which historical data is retained.
latitude (float, optional): The latitude in degrees, must be within -90 to 90.
longitude (float, optional): The longitude in degrees, must be within -180 to 180.
start_datetime (datetime, optional): The starting datetime for predictions, defaults to the current datetime if unspecified.
end_datetime (datetime, computed): The datetime representing the end of the prediction range,
calculated based on `start_datetime` and `prediction_hours`.
calculated based on `start_datetime` and `hours`.
keep_datetime (datetime, computed): The earliest datetime for retaining historical data, calculated
based on `start_datetime` and `prediction_historic_hours`.
based on `start_datetime` and `historic_hours`.
"""
# overload
@ -126,7 +126,7 @@ class WeatherProvider(PredictionProvider):
return "WeatherProvider"
def enabled(self) -> bool:
return self.provider_id() == self.config.weather.weather_provider
return self.provider_id() == self.config.weather.provider
@classmethod
def estimate_irradiance_from_cloud_cover(

View File

@ -62,13 +62,13 @@ class WeatherBrightSky(WeatherProvider):
of hours into the future and retains historical data.
Attributes:
prediction_hours (int, optional): Number of hours in the future for the forecast.
prediction_historic_hours (int, optional): Number of past hours for retaining data.
hours (int, optional): Number of hours in the future for the forecast.
historic_hours (int, optional): Number of past hours for retaining data.
latitude (float, optional): The latitude in degrees, validated to be between -90 and 90.
longitude (float, optional): The longitude in degrees, validated to be between -180 and 180.
start_datetime (datetime, optional): Start datetime for forecasts, defaults to the current datetime.
end_datetime (datetime, computed): The forecast's end datetime, computed based on `start_datetime` and `prediction_hours`.
keep_datetime (datetime, computed): The datetime to retain historical data, computed from `start_datetime` and `prediction_historic_hours`.
end_datetime (datetime, computed): The forecast's end datetime, computed based on `start_datetime` and `hours`.
keep_datetime (datetime, computed): The datetime to retain historical data, computed from `start_datetime` and `historic_hours`.
Methods:
provider_id(): Returns a unique identifier for the provider.

View File

@ -68,15 +68,15 @@ class WeatherClearOutside(WeatherProvider):
WeatherClearOutside is a thread-safe singleton, ensuring only one instance of this class is created.
Attributes:
prediction_hours (int, optional): The number of hours into the future for which predictions are generated.
prediction_historic_hours (int, optional): The number of past hours for which historical data is retained.
hours (int, optional): The number of hours into the future for which predictions are generated.
historic_hours (int, optional): The number of past hours for which historical data is retained.
latitude (float, optional): The latitude in degrees, must be within -90 to 90.
longitude (float, optional): The longitude in degrees, must be within -180 to 180.
start_datetime (datetime, optional): The starting datetime for predictions, defaults to the current datetime if unspecified.
end_datetime (datetime, computed): The datetime representing the end of the prediction range,
calculated based on `start_datetime` and `prediction_hours`.
calculated based on `start_datetime` and `hours`.
keep_datetime (datetime, computed): The earliest datetime for retaining historical data, calculated
based on `start_datetime` and `prediction_historic_hours`.
based on `start_datetime` and `historic_hours`.
"""
@classmethod

View File

@ -22,22 +22,22 @@ logger = get_logger(__name__)
class WeatherImportCommonSettings(SettingsBaseModel):
"""Common settings for weather data import from file or JSON string."""
weatherimport_file_path: Optional[Union[str, Path]] = Field(
import_file_path: Optional[Union[str, Path]] = Field(
default=None,
description="Path to the file to import weather data from.",
examples=[None, "/path/to/weather_data.json"],
)
weatherimport_json: Optional[str] = Field(
import_json: Optional[str] = Field(
default=None,
description="JSON string, dictionary of weather forecast value lists.",
examples=['{"weather_temp_air": [18.3, 17.8, 16.9]}'],
)
# Validators
@field_validator("weatherimport_file_path", mode="after")
@field_validator("import_file_path", mode="after")
@classmethod
def validate_weatherimport_file_path(cls, value: Optional[Union[str, Path]]) -> Optional[Path]:
def validate_import_file_path(cls, value: Optional[Union[str, Path]]) -> Optional[Path]:
if value is None:
return None
if isinstance(value, str):
@ -63,11 +63,11 @@ class WeatherImport(WeatherProvider, PredictionImportProvider):
return "WeatherImport"
def _update_data(self, force_update: Optional[bool] = False) -> None:
if self.config.weather.provider_settings.weatherimport_file_path is not None:
if self.config.weather.provider_settings.import_file_path is not None:
self.import_from_file(
self.config.weather.provider_settings.weatherimport_file_path, key_prefix="weather"
self.config.weather.provider_settings.import_file_path, key_prefix="weather"
)
if self.config.weather.provider_settings.weatherimport_json is not None:
if self.config.weather.provider_settings.import_json is not None:
self.import_from_json(
self.config.weather.provider_settings.weatherimport_json, key_prefix="weather"
self.config.weather.provider_settings.import_json, key_prefix="weather"
)

View File

@ -33,6 +33,7 @@ from akkudoktoreos.prediction.elecprice import ElecPriceCommonSettings
from akkudoktoreos.prediction.load import LoadCommonSettings
from akkudoktoreos.prediction.loadakkudoktor import LoadAkkudoktorCommonSettings
from akkudoktoreos.prediction.prediction import PredictionCommonSettings, get_prediction
from akkudoktoreos.prediction.pvforecast import PVForecastCommonSettings
from akkudoktoreos.utils.datetimeutil import to_datetime, to_duration
logger = get_logger(__name__)
@ -152,20 +153,16 @@ def start_eosdash() -> subprocess.Popen:
if args is None:
# No command line arguments
host = config_eos.server.server_eosdash_host
port = config_eos.server.server_eosdash_port
eos_host = config_eos.server.server_eos_host
eos_port = config_eos.server.server_eos_port
host = config_eos.server.eosdash_host
port = config_eos.server.eosdash_port
eos_host = config_eos.server.host
eos_port = config_eos.server.port
log_level = "info"
access_log = False
reload = False
else:
host = args.host
port = (
config_eos.server.server_eosdash_port
if config_eos.server.server_eosdash_port
else (args.port + 1)
)
port = config_eos.server.eosdash_port if config_eos.server.eosdash_port else (args.port + 1)
eos_host = args.host
eos_port = args.port
log_level = args.log_level
@ -208,7 +205,7 @@ def start_eosdash() -> subprocess.Popen:
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
"""Lifespan manager for the app."""
# On startup
if config_eos.server.server_eos_startup_eosdash:
if config_eos.server.startup_eosdash:
try:
eosdash_process = start_eosdash()
except Exception as e:
@ -235,7 +232,7 @@ app = FastAPI(
# That's the problem
opt_class = optimization_problem(verbose=bool(config_eos.server.server_eos_verbose))
opt_class = optimization_problem(verbose=bool(config_eos.server.verbose))
server_dir = Path(__file__).parent.resolve()
@ -610,7 +607,7 @@ def fastapi_strompreis() -> list[float]:
Electricity price charges are added.
Note:
Set ElecPriceAkkudoktor as elecprice_provider, then update data with
Set ElecPriceAkkudoktor as provider, then update data with
'/v1/prediction/update'
and then request data with
'/v1/prediction/list?key=elecprice_marketprice_wh' or
@ -618,7 +615,7 @@ def fastapi_strompreis() -> list[float]:
"""
settings = SettingsEOS(
elecprice=ElecPriceCommonSettings(
elecprice_provider="ElecPriceAkkudoktor",
provider="ElecPriceAkkudoktor",
)
)
config_eos.merge_settings(settings=settings)
@ -670,10 +667,10 @@ def fastapi_gesamtlast(request: GesamtlastRequest) -> list[float]:
"""
settings = SettingsEOS(
prediction=PredictionCommonSettings(
prediction_hours=request.hours,
hours=request.hours,
),
load=LoadCommonSettings(
load_provider="LoadAkkudoktor",
provider="LoadAkkudoktor",
provider_settings=LoadAkkudoktorCommonSettings(
loadakkudoktor_year_energy=request.year_energy,
),
@ -684,7 +681,7 @@ def fastapi_gesamtlast(request: GesamtlastRequest) -> list[float]:
# Insert measured data into EOS measurement
# Convert from energy per interval to dummy energy meter readings
measurement_key = "measurement_load0_mr"
measurement_key = "load0_mr"
measurement_eos.key_delete_by_datetime(key=measurement_key) # delete all load0_mr measurements
energy = {}
try:
@ -747,14 +744,14 @@ def fastapi_gesamtlast_simple(year_energy: float) -> list[float]:
year_energy (float): Yearly energy consumption in Wh.
Note:
Set LoadAkkudoktor as load_provider, then update data with
Set LoadAkkudoktor as provider, then update data with
'/v1/prediction/update'
and then request data with
'/v1/prediction/list?key=load_mean' instead.
"""
settings = SettingsEOS(
load=LoadCommonSettings(
load_provider="LoadAkkudoktor",
provider="LoadAkkudoktor",
provider_settings=LoadAkkudoktorCommonSettings(
loadakkudoktor_year_energy=year_energy / 1000, # Convert to kWh
),
@ -800,21 +797,25 @@ def fastapi_pvforecast() -> ForecastResponse:
filled with the first available forecast value.
Note:
Set PVForecastAkkudoktor as pvforecast_provider, then update data with
Set PVForecastAkkudoktor as provider, then update data with
'/v1/prediction/update'
and then request data with
'/v1/prediction/list?key=pvforecast_ac_power' and
'/v1/prediction/list?key=pvforecastakkudoktor_temp_air' instead.
"""
settings = SettingsEOS(
elecprice_provider="PVForecastAkkudoktor",
)
settings = SettingsEOS(pvforecast=PVForecastCommonSettings(provider="PVForecastAkkudoktor"))
config_eos.merge_settings(settings=settings)
ems_eos.set_start_datetime() # Set energy management start datetime to current hour.
# Create PV forecast
prediction_eos.update_data(force_update=True)
try:
prediction_eos.update_data(force_update=True)
except ValueError as e:
raise HTTPException(
status_code=404,
detail=f"Can not get the PV forecast: {e}",
)
# Get the forcast starting at start of day
start_datetime = to_datetime().start_of("day")
@ -901,9 +902,9 @@ async def proxy_put(request: Request, path: str) -> Response:
async def proxy(request: Request, path: str) -> Union[Response | RedirectResponse | HTMLResponse]:
if config_eos.server.server_eosdash_host and config_eos.server.server_eosdash_port:
if config_eos.server.eosdash_host and config_eos.server.eosdash_port:
# Proxy to EOSdash server
url = f"http://{config_eos.server.server_eosdash_host}:{config_eos.server.server_eosdash_port}/{path}"
url = f"http://{config_eos.server.eosdash_host}:{config_eos.server.eosdash_port}/{path}"
headers = dict(request.headers)
data = await request.body()
@ -925,9 +926,9 @@ async def proxy(request: Request, path: str) -> Union[Response | RedirectRespons
error_message=f"""<pre>
EOSdash server not reachable: '{url}'
Did you start the EOSdash server
or set 'server_eos_startup_eosdash'?
or set 'startup_eosdash'?
If there is no application server intended please
set 'server_eosdash_host' or 'server_eosdash_port' to None.
set 'eosdash_host' or 'eosdash_port' to None.
</pre>
""",
error_details=f"{e}",
@ -991,8 +992,8 @@ def main() -> None:
it starts the EOS server with the specified configurations.
Command-line Arguments:
--host (str): Host for the EOS server (default: value from config_eos).
--port (int): Port for the EOS server (default: value from config_eos).
--host (str): Host for the EOS server (default: value from config).
--port (int): Port for the EOS server (default: value from config).
--log_level (str): Log level for the server. Options: "critical", "error", "warning", "info", "debug", "trace" (default: "info").
--access_log (bool): Enable or disable access log. Options: True or False (default: False).
--reload (bool): Enable or disable auto-reload. Useful for development. Options: True or False (default: False).
@ -1003,13 +1004,13 @@ def main() -> None:
parser.add_argument(
"--host",
type=str,
default=str(config_eos.server.server_eos_host),
default=str(config_eos.server.host),
help="Host for the EOS server (default: value from config)",
)
parser.add_argument(
"--port",
type=int,
default=config_eos.server.server_eos_port,
default=config_eos.server.port,
help="Port for the EOS server (default: value from config)",
)
@ -1038,7 +1039,7 @@ def main() -> None:
try:
run_eos(args.host, args.port, args.log_level, args.access_log, args.reload)
except:
exit(1)
sys.exit(1)
if __name__ == "__main__":

View File

@ -1,5 +1,6 @@
import argparse
import os
import sys
from functools import reduce
from typing import Any, Union
@ -165,10 +166,10 @@ def main() -> None:
it starts the EOSdash server with the specified configurations.
Command-line Arguments:
--host (str): Host for the EOSdash server (default: value from config_eos).
--port (int): Port for the EOSdash server (default: value from config_eos).
--eos-host (str): Host for the EOS server (default: value from config_eos).
--eos-port (int): Port for the EOS server (default: value from config_eos).
--host (str): Host for the EOSdash server (default: value from config).
--port (int): Port for the EOSdash server (default: value from config).
--eos-host (str): Host for the EOS server (default: value from config).
--eos-port (int): Port for the EOS server (default: value from config).
--log_level (str): Log level for the server. Options: "critical", "error", "warning", "info", "debug", "trace" (default: "info").
--access_log (bool): Enable or disable access log. Options: True or False (default: False).
--reload (bool): Enable or disable auto-reload. Useful for development. Options: True or False (default: False).
@ -179,28 +180,28 @@ def main() -> None:
parser.add_argument(
"--host",
type=str,
default=str(config_eos.server.server_eosdash_host),
help="Host for the EOSdash server (default: value from config_eos)",
default=str(config_eos.server.eosdash_host),
help="Host for the EOSdash server (default: value from config)",
)
parser.add_argument(
"--port",
type=int,
default=config_eos.server.server_eosdash_port,
help="Port for the EOSdash server (default: value from config_eos)",
default=config_eos.server.eosdash_port,
help="Port for the EOSdash server (default: value from config)",
)
# EOS Host and port arguments with defaults from config_eos
parser.add_argument(
"--eos-host",
type=str,
default=str(config_eos.server.server_eos_host),
help="Host for the EOS server (default: value from config_eos)",
default=str(config_eos.server.host),
help="Host for the EOS server (default: value from config)",
)
parser.add_argument(
"--eos-port",
type=int,
default=config_eos.server.server_eos_port,
help="Port for the EOS server (default: value from config_eos)",
default=config_eos.server.port,
help="Port for the EOS server (default: value from config)",
)
# Optional arguments for log_level, access_log, and reload
@ -228,7 +229,7 @@ def main() -> None:
try:
run_eosdash(args.host, args.port, args.log_level, args.access_log, args.reload)
except:
exit(1)
sys.exit(1)
if __name__ == "__main__":

View File

@ -17,22 +17,18 @@ class ServerCommonSettings(SettingsBaseModel):
To be added
"""
server_eos_host: Optional[IPvAnyAddress] = Field(
default="0.0.0.0", description="EOS server IP address."
)
server_eos_port: Optional[int] = Field(default=8503, description="EOS server IP port number.")
server_eos_verbose: Optional[bool] = Field(default=False, description="Enable debug output")
server_eos_startup_eosdash: Optional[bool] = Field(
host: Optional[IPvAnyAddress] = Field(default="0.0.0.0", description="EOS server IP address.")
port: Optional[int] = Field(default=8503, description="EOS server IP port number.")
verbose: Optional[bool] = Field(default=False, description="Enable debug output")
startup_eosdash: Optional[bool] = Field(
default=True, description="EOS server to start EOSdash server."
)
server_eosdash_host: Optional[IPvAnyAddress] = Field(
eosdash_host: Optional[IPvAnyAddress] = Field(
default="0.0.0.0", description="EOSdash server IP address."
)
server_eosdash_port: Optional[int] = Field(
default=8504, description="EOSdash server IP port number."
)
eosdash_port: Optional[int] = Field(default=8504, description="EOSdash server IP port number.")
@field_validator("server_eos_port", "server_eosdash_port")
@field_validator("port", "eosdash_port")
def validate_server_port(cls, value: Optional[int]) -> Optional[int]:
if value is not None and not (1024 <= value <= 49151):
raise ValueError("Server port number must be between 1024 and 49151.")

View File

@ -57,6 +57,6 @@ class NumpyEncoder(json.JSONEncoder):
# # Example usage
# start_date = datetime.datetime(2024, 3, 31) # Date of the DST change
# if ist_dst_wechsel(start_date):
# prediction_hours = 23 # Adjust to 23 hours for DST change days
# hours = 23 # Adjust to 23 hours for DST change days
# else:
# prediction_hours = 24 # Default value for days without DST change
# hours = 24 # Default value for days without DST change

View File

@ -24,9 +24,9 @@ def create_ems_instance(devices_eos, config_eos) -> EnergieManagementSystem:
"""Fixture to create an EnergieManagementSystem instance with given test parameters."""
# Assure configuration holds the correct values
config_eos.merge_settings_from_dict(
{"prediction": {"prediction_hours": 48}, "optimization": {"optimization_hours": 24}}
{"prediction": {"hours": 48}, "optimization": {"hours": 24}}
)
assert config_eos.prediction.prediction_hours == 48
assert config_eos.prediction.hours == 48
# Initialize the battery and the inverter
akku = Battery(
@ -41,7 +41,7 @@ def create_ems_instance(devices_eos, config_eos) -> EnergieManagementSystem:
devices_eos.add_device(akku)
inverter = Inverter(
InverterParameters(device_id="inverter1", max_power_wh=10000, battery=akku.device_id)
InverterParameters(device_id="inverter1", max_power_wh=10000, battery_id=akku.device_id)
)
devices_eos.add_device(inverter)
@ -62,7 +62,7 @@ def create_ems_instance(devices_eos, config_eos) -> EnergieManagementSystem:
device_id="ev1", capacity_wh=26400, initial_soc_percentage=10, min_soc_percentage=10
),
)
eauto.set_charge_per_hour(np.full(config_eos.prediction.prediction_hours, 1))
eauto.set_charge_per_hour(np.full(config_eos.prediction.hours, 1))
devices_eos.add_device(eauto)
devices_eos.post_setup()

View File

@ -23,9 +23,9 @@ def create_ems_instance(devices_eos, config_eos) -> EnergieManagementSystem:
"""Fixture to create an EnergieManagementSystem instance with given test parameters."""
# Assure configuration holds the correct values
config_eos.merge_settings_from_dict(
{"prediction": {"prediction_hours": 48}, "optimization": {"optimization_hours": 24}}
{"prediction": {"hours": 48}, "optimization": {"hours": 24}}
)
assert config_eos.prediction.prediction_hours == 48
assert config_eos.prediction.hours == 48
# Initialize the battery and the inverter
akku = Battery(
@ -37,7 +37,7 @@ def create_ems_instance(devices_eos, config_eos) -> EnergieManagementSystem:
devices_eos.add_device(akku)
inverter = Inverter(
InverterParameters(device_id="iv1", max_power_wh=10000, battery=akku.device_id)
InverterParameters(device_id="iv1", max_power_wh=10000, battery_id=akku.device_id)
)
devices_eos.add_device(inverter)
@ -63,11 +63,11 @@ def create_ems_instance(devices_eos, config_eos) -> EnergieManagementSystem:
devices_eos.post_setup()
# Parameters based on previous example data
pv_prognose_wh = [0.0] * config_eos.prediction.prediction_hours
pv_prognose_wh = [0.0] * config_eos.prediction.hours
pv_prognose_wh[10] = 5000.0
pv_prognose_wh[11] = 5000.0
strompreis_euro_pro_wh = [0.001] * config_eos.prediction.prediction_hours
strompreis_euro_pro_wh = [0.001] * config_eos.prediction.hours
strompreis_euro_pro_wh[0:10] = [0.00001] * 10
strompreis_euro_pro_wh[11:15] = [0.00005] * 4
strompreis_euro_pro_wh[20] = 0.00001
@ -141,10 +141,10 @@ def create_ems_instance(devices_eos, config_eos) -> EnergieManagementSystem:
home_appliance=home_appliance,
)
ac = np.full(config_eos.prediction.prediction_hours, 0.0)
ac = np.full(config_eos.prediction.hours, 0.0)
ac[20] = 1
ems.set_akku_ac_charge_hours(ac)
dc = np.full(config_eos.prediction.prediction_hours, 0.0)
dc = np.full(config_eos.prediction.hours, 0.0)
dc[11] = 1
ems.set_akku_dc_charge_hours(dc)

View File

@ -50,7 +50,7 @@ def test_optimize(
"""Test optimierung_ems."""
# Assure configuration holds the correct values
config_eos.merge_settings_from_dict(
{"prediction": {"prediction_hours": 48}, "optimization": {"optimization_hours": 48}}
{"prediction": {"hours": 48}, "optimization": {"hours": 48}}
)
# Load input and output data

View File

@ -23,7 +23,7 @@ FILE_TESTDATA_ELECPRICEAKKUDOKTOR_1_JSON = DIR_TESTDATA.joinpath(
@pytest.fixture
def elecprice_provider(monkeypatch, config_eos):
def provider(monkeypatch, config_eos):
"""Fixture to create a ElecPriceProvider instance."""
monkeypatch.setenv("EOS_ELECPRICE__ELECPRICE_PROVIDER", "ElecPriceAkkudoktor")
config_eos.reset_settings()
@ -49,17 +49,17 @@ def cache_store():
# ------------------------------------------------
def test_singleton_instance(elecprice_provider):
def test_singleton_instance(provider):
"""Test that ElecPriceForecast behaves as a singleton."""
another_instance = ElecPriceAkkudoktor()
assert elecprice_provider is another_instance
assert provider is another_instance
def test_invalid_provider(elecprice_provider, monkeypatch):
"""Test requesting an unsupported elecprice_provider."""
def test_invalid_provider(provider, monkeypatch):
"""Test requesting an unsupported provider."""
monkeypatch.setenv("EOS_ELECPRICE__ELECPRICE_PROVIDER", "<invalid>")
elecprice_provider.config.reset_settings()
assert not elecprice_provider.enabled()
provider.config.reset_settings()
assert not provider.enabled()
# ------------------------------------------------
@ -68,16 +68,16 @@ def test_invalid_provider(elecprice_provider, monkeypatch):
@patch("akkudoktoreos.prediction.elecpriceakkudoktor.logger.error")
def test_validate_data_invalid_format(mock_logger, elecprice_provider):
def test_validate_data_invalid_format(mock_logger, provider):
"""Test validation for invalid Akkudoktor data."""
invalid_data = '{"invalid": "data"}'
with pytest.raises(ValueError):
elecprice_provider._validate_data(invalid_data)
provider._validate_data(invalid_data)
mock_logger.assert_called_once_with(mock_logger.call_args[0][0])
@patch("requests.get")
def test_request_forecast(mock_get, elecprice_provider, sample_akkudoktor_1_json):
def test_request_forecast(mock_get, provider, sample_akkudoktor_1_json):
"""Test requesting forecast from Akkudoktor."""
# Mock response object
mock_response = Mock()
@ -86,10 +86,10 @@ def test_request_forecast(mock_get, elecprice_provider, sample_akkudoktor_1_json
mock_get.return_value = mock_response
# Preset, as this is usually done by update()
elecprice_provider.config.update()
provider.config.update()
# Test function
akkudoktor_data = elecprice_provider._request_forecast()
akkudoktor_data = provider._request_forecast()
assert isinstance(akkudoktor_data, AkkudoktorElecPrice)
assert akkudoktor_data.values[0] == AkkudoktorElecPriceValue(
@ -104,7 +104,7 @@ def test_request_forecast(mock_get, elecprice_provider, sample_akkudoktor_1_json
@patch("requests.get")
def test_update_data(mock_get, elecprice_provider, sample_akkudoktor_1_json, cache_store):
def test_update_data(mock_get, provider, sample_akkudoktor_1_json, cache_store):
"""Test fetching forecast from Akkudoktor."""
# Mock response object
mock_response = Mock()
@ -117,28 +117,28 @@ def test_update_data(mock_get, elecprice_provider, sample_akkudoktor_1_json, cac
# Call the method
ems_eos = get_ems()
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)
provider.update_data(force_enable=True, force_update=True)
# Assert: Verify the result is as expected
mock_get.assert_called_once()
assert (
len(elecprice_provider) == 73
len(provider) == 73
) # we have 48 datasets in the api response, we want to know 48h into the future. The data we get has already 23h into the future so we need only 25h more. 48+25=73
# Assert we get prediction_hours prioce values by resampling
np_price_array = elecprice_provider.key_to_array(
# Assert we get hours prioce values by resampling
np_price_array = provider.key_to_array(
key="elecprice_marketprice_wh",
start_datetime=elecprice_provider.start_datetime,
end_datetime=elecprice_provider.end_datetime,
start_datetime=provider.start_datetime,
end_datetime=provider.end_datetime,
)
assert len(np_price_array) == elecprice_provider.total_hours
assert len(np_price_array) == provider.total_hours
# with open(FILE_TESTDATA_ELECPRICEAKKUDOKTOR_2_JSON, "w") as f_out:
# f_out.write(elecprice_provider.to_json())
# f_out.write(provider.to_json())
@patch("requests.get")
def test_update_data_with_incomplete_forecast(mock_get, elecprice_provider):
def test_update_data_with_incomplete_forecast(mock_get, provider):
"""Test `_update_data` with incomplete or missing forecast data."""
incomplete_data: dict = {"meta": {}, "values": []}
mock_response = Mock()
@ -146,7 +146,7 @@ def test_update_data_with_incomplete_forecast(mock_get, elecprice_provider):
mock_response.content = json.dumps(incomplete_data)
mock_get.return_value = mock_response
with pytest.raises(ValueError):
elecprice_provider._update_data(force_update=True)
provider._update_data(force_update=True)
@pytest.mark.parametrize(
@ -155,7 +155,7 @@ def test_update_data_with_incomplete_forecast(mock_get, elecprice_provider):
)
@patch("requests.get")
def test_request_forecast_status_codes(
mock_get, elecprice_provider, sample_akkudoktor_1_json, status_code, exception
mock_get, provider, sample_akkudoktor_1_json, status_code, exception
):
"""Test handling of various API status codes."""
mock_response = Mock()
@ -167,31 +167,31 @@ def test_request_forecast_status_codes(
mock_get.return_value = mock_response
if exception:
with pytest.raises(exception):
elecprice_provider._request_forecast()
provider._request_forecast()
else:
elecprice_provider._request_forecast()
provider._request_forecast()
@patch("akkudoktoreos.utils.cacheutil.CacheFileStore")
def test_cache_integration(mock_cache, elecprice_provider):
def test_cache_integration(mock_cache, provider):
"""Test caching of 8-day electricity price data."""
mock_cache_instance = mock_cache.return_value
mock_cache_instance.get.return_value = None # Simulate no cache
elecprice_provider._update_data(force_update=True)
provider._update_data(force_update=True)
mock_cache_instance.create.assert_called_once()
mock_cache_instance.get.assert_called_once()
def test_key_to_array_resampling(elecprice_provider):
def test_key_to_array_resampling(provider):
"""Test resampling of forecast data to NumPy array."""
elecprice_provider.update_data(force_update=True)
array = elecprice_provider.key_to_array(
provider.update_data(force_update=True)
array = provider.key_to_array(
key="elecprice_marketprice_wh",
start_datetime=elecprice_provider.start_datetime,
end_datetime=elecprice_provider.end_datetime,
start_datetime=provider.start_datetime,
end_datetime=provider.end_datetime,
)
assert isinstance(array, np.ndarray)
assert len(array) == elecprice_provider.total_hours
assert len(array) == provider.total_hours
# ------------------------------------------------
@ -200,12 +200,12 @@ def test_key_to_array_resampling(elecprice_provider):
@pytest.mark.skip(reason="For development only")
def test_akkudoktor_development_forecast_data(elecprice_provider):
def test_akkudoktor_development_forecast_data(provider):
"""Fetch data from real Akkudoktor server."""
# Preset, as this is usually done by update_data()
elecprice_provider.start_datetime = to_datetime("2024-10-26 00:00:00")
provider.start_datetime = to_datetime("2024-10-26 00:00:00")
akkudoktor_data = elecprice_provider._request_forecast()
akkudoktor_data = provider._request_forecast()
with open(FILE_TESTDATA_ELECPRICEAKKUDOKTOR_1_JSON, "w") as f_out:
json.dump(akkudoktor_data, f_out, indent=4)

View File

@ -13,14 +13,14 @@ FILE_TESTDATA_ELECPRICEIMPORT_1_JSON = DIR_TESTDATA.joinpath("import_input_1.jso
@pytest.fixture
def elecprice_provider(sample_import_1_json, config_eos):
def provider(sample_import_1_json, config_eos):
"""Fixture to create a ElecPriceProvider instance."""
settings = {
"elecprice": {
"elecprice_provider": "ElecPriceImport",
"provider": "ElecPriceImport",
"provider_settings": {
"elecpriceimport_file_path": str(FILE_TESTDATA_ELECPRICEIMPORT_1_JSON),
"elecpriceimport_json": json.dumps(sample_import_1_json),
"import_file_path": str(FILE_TESTDATA_ELECPRICEIMPORT_1_JSON),
"import_json": json.dumps(sample_import_1_json),
},
}
}
@ -43,24 +43,24 @@ def sample_import_1_json():
# ------------------------------------------------
def test_singleton_instance(elecprice_provider):
def test_singleton_instance(provider):
"""Test that ElecPriceForecast behaves as a singleton."""
another_instance = ElecPriceImport()
assert elecprice_provider is another_instance
assert provider is another_instance
def test_invalid_provider(elecprice_provider, config_eos):
"""Test requesting an unsupported elecprice_provider."""
def test_invalid_provider(provider, config_eos):
"""Test requesting an unsupported provider."""
settings = {
"elecprice": {
"elecprice_provider": "<invalid>",
"provider": "<invalid>",
"provider_settings": {
"elecpriceimport_file_path": str(FILE_TESTDATA_ELECPRICEIMPORT_1_JSON),
"import_file_path": str(FILE_TESTDATA_ELECPRICEIMPORT_1_JSON),
},
}
}
config_eos.merge_settings_from_dict(settings)
assert not elecprice_provider.enabled()
assert not provider.enabled()
# ------------------------------------------------
@ -81,35 +81,33 @@ def test_invalid_provider(elecprice_provider, config_eos):
("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, config_eos):
def test_import(provider, sample_import_1_json, start_datetime, from_file, config_eos):
"""Test fetching forecast from Import."""
ems_eos = get_ems()
ems_eos.set_start_datetime(to_datetime(start_datetime, in_timezone="Europe/Berlin"))
if from_file:
config_eos.elecprice.provider_settings.elecpriceimport_json = None
assert config_eos.elecprice.provider_settings.elecpriceimport_json is None
config_eos.elecprice.provider_settings.import_json = None
assert config_eos.elecprice.provider_settings.import_json is None
else:
config_eos.elecprice.provider_settings.elecpriceimport_file_path = None
assert config_eos.elecprice.provider_settings.elecpriceimport_file_path is None
elecprice_provider.clear()
config_eos.elecprice.provider_settings.import_file_path = None
assert config_eos.elecprice.provider_settings.import_file_path is None
provider.clear()
# Call the method
elecprice_provider.update_data()
provider.update_data()
# Assert: Verify the result is as expected
assert elecprice_provider.start_datetime is not None
assert elecprice_provider.total_hours is not None
assert compare_datetimes(elecprice_provider.start_datetime, ems_eos.start_datetime).equal
assert provider.start_datetime is not None
assert provider.total_hours is not None
assert compare_datetimes(provider.start_datetime, ems_eos.start_datetime).equal
values = sample_import_1_json["elecprice_marketprice_wh"]
value_datetime_mapping = elecprice_provider.import_datetimes(
ems_eos.start_datetime, len(values)
)
value_datetime_mapping = provider.import_datetimes(ems_eos.start_datetime, len(values))
for i, mapping in enumerate(value_datetime_mapping):
assert i < len(elecprice_provider.records)
assert i < len(provider.records)
expected_datetime, expected_value_index = mapping
expected_value = values[expected_value_index]
result_datetime = elecprice_provider.records[i].date_time
result_value = elecprice_provider.records[i]["elecprice_marketprice_wh"]
result_datetime = provider.records[i].date_time
result_value = provider.records[i]["elecprice_marketprice_wh"]
# print(f"{i}: Expected: {expected_datetime}:{expected_value}")
# print(f"{i}: Result: {result_datetime}:{result_value}")

View File

@ -24,7 +24,9 @@ def inverter(mock_battery, devices_eos) -> Inverter:
return_value=mock_self_consumption_predictor,
):
iv = Inverter(
InverterParameters(device_id="iv1", max_power_wh=500.0, battery=mock_battery.device_id),
InverterParameters(
device_id="iv1", max_power_wh=500.0, battery_id=mock_battery.device_id
),
)
devices_eos.add_device(iv)
devices_eos.post_setup()

View File

@ -14,11 +14,11 @@ from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime, to_
@pytest.fixture
def load_provider(config_eos):
def provider(config_eos):
"""Fixture to initialise the LoadAkkudoktor instance."""
settings = {
"load": {
"load_provider": "LoadAkkudoktor",
"provider": "LoadAkkudoktor",
"provider_settings": {
"load_name": "Akkudoktor Profile",
"loadakkudoktor_year_energy": "1000",
@ -41,8 +41,8 @@ def measurement_eos():
measurement.records.append(
MeasurementDataRecord(
date_time=dt,
measurement_load0_mr=load0_mr,
measurement_load1_mr=load1_mr,
load0_mr=load0_mr,
load1_mr=load1_mr,
)
)
dt += interval
@ -76,13 +76,13 @@ def test_loadakkudoktor_settings_validator():
assert settings.loadakkudoktor_year_energy == 1234.56
def test_loadakkudoktor_provider_id(load_provider):
def test_loadakkudoktor_provider_id(provider):
"""Test the `provider_id` class method."""
assert load_provider.provider_id() == "LoadAkkudoktor"
assert provider.provider_id() == "LoadAkkudoktor"
@patch("akkudoktoreos.prediction.loadakkudoktor.np.load")
def test_load_data_from_mock(mock_np_load, mock_load_profiles_file, load_provider):
def test_load_data_from_mock(mock_np_load, mock_load_profiles_file, provider):
"""Test the `load_data` method."""
# Mock numpy load to return data similar to what would be in the file
mock_np_load.return_value = {
@ -91,19 +91,19 @@ def test_load_data_from_mock(mock_np_load, mock_load_profiles_file, load_provide
}
# Test data loading
data_year_energy = load_provider.load_data()
data_year_energy = provider.load_data()
assert data_year_energy is not None
assert data_year_energy.shape == (365, 2, 24)
def test_load_data_from_file(load_provider):
def test_load_data_from_file(provider):
"""Test `load_data` loads data from the profiles file."""
data_year_energy = load_provider.load_data()
data_year_energy = provider.load_data()
assert data_year_energy is not None
@patch("akkudoktoreos.prediction.loadakkudoktor.LoadAkkudoktor.load_data")
def test_update_data(mock_load_data, load_provider):
def test_update_data(mock_load_data, provider):
"""Test the `_update` method."""
mock_load_data.return_value = np.random.rand(365, 2, 24)
@ -112,27 +112,27 @@ def test_update_data(mock_load_data, load_provider):
ems_eos.set_start_datetime(pendulum.datetime(2024, 1, 1))
# Assure there are no prediction records
load_provider.clear()
assert len(load_provider) == 0
provider.clear()
assert len(provider) == 0
# Execute the method
load_provider._update_data()
provider._update_data()
# Validate that update_value is called
assert len(load_provider) > 0
assert len(provider) > 0
def test_calculate_adjustment(load_provider, measurement_eos):
def test_calculate_adjustment(provider, measurement_eos):
"""Test `_calculate_adjustment` for various scenarios."""
data_year_energy = np.random.rand(365, 2, 24)
# Call the method and validate results
weekday_adjust, weekend_adjust = load_provider._calculate_adjustment(data_year_energy)
weekday_adjust, weekend_adjust = provider._calculate_adjustment(data_year_energy)
assert weekday_adjust.shape == (24,)
assert weekend_adjust.shape == (24,)
data_year_energy = np.zeros((365, 2, 24))
weekday_adjust, weekend_adjust = load_provider._calculate_adjustment(data_year_energy)
weekday_adjust, weekend_adjust = provider._calculate_adjustment(data_year_energy)
assert weekday_adjust.shape == (24,)
expected = np.array(
@ -197,7 +197,7 @@ def test_calculate_adjustment(load_provider, measurement_eos):
np.testing.assert_array_equal(weekend_adjust, expected)
def test_load_provider_adjustments_with_mock_data(load_provider):
def test_provider_adjustments_with_mock_data(provider):
"""Test full integration of adjustments with mock data."""
with patch(
"akkudoktoreos.prediction.loadakkudoktor.LoadAkkudoktor._calculate_adjustment"
@ -205,5 +205,5 @@ def test_load_provider_adjustments_with_mock_data(load_provider):
mock_adjust.return_value = (np.zeros(24), np.zeros(24))
# Test execution
load_provider._update_data()
provider._update_data()
assert mock_adjust.called

View File

@ -17,33 +17,33 @@ def measurement_eos():
measurement.records = [
MeasurementDataRecord(
date_time=datetime(2023, 1, 1, hour=0),
measurement_load0_mr=100,
measurement_load1_mr=200,
load0_mr=100,
load1_mr=200,
),
MeasurementDataRecord(
date_time=datetime(2023, 1, 1, hour=1),
measurement_load0_mr=150,
measurement_load1_mr=250,
load0_mr=150,
load1_mr=250,
),
MeasurementDataRecord(
date_time=datetime(2023, 1, 1, hour=2),
measurement_load0_mr=200,
measurement_load1_mr=300,
load0_mr=200,
load1_mr=300,
),
MeasurementDataRecord(
date_time=datetime(2023, 1, 1, hour=3),
measurement_load0_mr=250,
measurement_load1_mr=350,
load0_mr=250,
load1_mr=350,
),
MeasurementDataRecord(
date_time=datetime(2023, 1, 1, hour=4),
measurement_load0_mr=300,
measurement_load1_mr=400,
load0_mr=300,
load1_mr=400,
),
MeasurementDataRecord(
date_time=datetime(2023, 1, 1, hour=5),
measurement_load0_mr=350,
measurement_load1_mr=450,
load0_mr=350,
load1_mr=450,
),
]
return measurement
@ -79,7 +79,7 @@ def test_interval_count_invalid_non_positive_interval(measurement_eos):
def test_energy_from_meter_readings_valid_input(measurement_eos):
"""Test _energy_from_meter_readings with valid inputs and proper alignment of load data."""
key = "measurement_load0_mr"
key = "load0_mr"
start_datetime = datetime(2023, 1, 1, 0)
end_datetime = datetime(2023, 1, 1, 5)
interval = duration(hours=1)
@ -94,7 +94,7 @@ def test_energy_from_meter_readings_valid_input(measurement_eos):
def test_energy_from_meter_readings_empty_array(measurement_eos):
"""Test _energy_from_meter_readings with no data (empty array)."""
key = "measurement_load0_mr"
key = "load0_mr"
start_datetime = datetime(2023, 1, 1, 0)
end_datetime = datetime(2023, 1, 1, 5)
interval = duration(hours=1)
@ -116,7 +116,7 @@ def test_energy_from_meter_readings_empty_array(measurement_eos):
def test_energy_from_meter_readings_misaligned_array(measurement_eos):
"""Test _energy_from_meter_readings with misaligned array size."""
key = "measurement_load1_mr"
key = "load1_mr"
start_datetime = measurement_eos.min_datetime
end_datetime = measurement_eos.max_datetime
interval = duration(hours=1)
@ -134,7 +134,7 @@ def test_energy_from_meter_readings_misaligned_array(measurement_eos):
def test_energy_from_meter_readings_partial_data(measurement_eos, caplog):
"""Test _energy_from_meter_readings with partial data (misaligned but empty array)."""
key = "measurement_load2_mr"
key = "load2_mr"
start_datetime = datetime(2023, 1, 1, 0)
end_datetime = datetime(2023, 1, 1, 5)
interval = duration(hours=1)
@ -153,7 +153,7 @@ def test_energy_from_meter_readings_partial_data(measurement_eos, caplog):
def test_energy_from_meter_readings_negative_interval(measurement_eos):
"""Test _energy_from_meter_readings with a negative interval."""
key = "measurement_load3_mr"
key = "load3_mr"
start_datetime = datetime(2023, 1, 1, 0)
end_datetime = datetime(2023, 1, 1, 5)
interval = duration(hours=-1)
@ -191,23 +191,23 @@ def test_name_to_key(measurement_eos):
"""Test name_to_key functionality."""
settings = SettingsEOS(
measurement=MeasurementCommonSettings(
measurement_load0_name="Household",
measurement_load1_name="Heat Pump",
load0_name="Household",
load1_name="Heat Pump",
)
)
measurement_eos.config.merge_settings(settings)
assert measurement_eos.name_to_key("Household", "measurement_load") == "measurement_load0_mr"
assert measurement_eos.name_to_key("Heat Pump", "measurement_load") == "measurement_load1_mr"
assert measurement_eos.name_to_key("Unknown", "measurement_load") is None
assert measurement_eos.name_to_key("Household", "load") == "load0_mr"
assert measurement_eos.name_to_key("Heat Pump", "load") == "load1_mr"
assert measurement_eos.name_to_key("Unknown", "load") is None
def test_name_to_key_invalid_topic(measurement_eos):
"""Test name_to_key with an invalid topic."""
settings = SettingsEOS(
MeasurementCommonSettings(
measurement_load0_name="Household",
measurement_load1_name="Heat Pump",
load0_name="Household",
load1_name="Heat Pump",
)
)
measurement_eos.config.merge_settings(settings)

View File

@ -17,25 +17,6 @@ from akkudoktoreos.prediction.weatherclearoutside import WeatherClearOutside
from akkudoktoreos.prediction.weatherimport import WeatherImport
@pytest.fixture
def sample_settings(config_eos):
"""Fixture that adds settings data to the global config."""
settings = {
"prediction_hours": 48,
"prediction_historic_hours": 24,
"latitude": 52.52,
"longitude": 13.405,
"weather_provider": None,
"pvforecast_provider": None,
"load_provider": None,
"elecprice_provider": None,
}
# Merge settings to config
config_eos.merge_settings_from_dict(settings)
return config_eos
@pytest.fixture
def prediction():
"""All EOS predictions."""
@ -59,7 +40,7 @@ def forecast_providers():
@pytest.mark.parametrize(
"prediction_hours, prediction_historic_hours, latitude, longitude, expected_timezone",
"hours, historic_hours, latitude, longitude, expected_timezone",
[
(48, 24, 40.7128, -74.0060, "America/New_York"), # Valid latitude/longitude
(0, 0, None, None, None), # No location
@ -67,17 +48,17 @@ def forecast_providers():
],
)
def test_prediction_common_settings_valid(
prediction_hours, prediction_historic_hours, latitude, longitude, expected_timezone
hours, historic_hours, latitude, longitude, expected_timezone
):
"""Test valid settings for PredictionCommonSettings."""
settings = PredictionCommonSettings(
prediction_hours=prediction_hours,
prediction_historic_hours=prediction_historic_hours,
hours=hours,
historic_hours=historic_hours,
latitude=latitude,
longitude=longitude,
)
assert settings.prediction_hours == prediction_hours
assert settings.prediction_historic_hours == prediction_historic_hours
assert settings.hours == hours
assert settings.historic_hours == historic_hours
assert settings.latitude == latitude
assert settings.longitude == longitude
assert settings.timezone == expected_timezone
@ -86,8 +67,8 @@ def test_prediction_common_settings_valid(
@pytest.mark.parametrize(
"field_name, invalid_value, expected_error",
[
("prediction_hours", -1, "Input should be greater than or equal to 0"),
("prediction_historic_hours", -5, "Input should be greater than or equal to 0"),
("hours", -1, "Input should be greater than or equal to 0"),
("historic_hours", -5, "Input should be greater than or equal to 0"),
("latitude", -91.0, "Input should be greater than or equal to -90"),
("latitude", 91.0, "Input should be less than or equal to 90"),
("longitude", -181.0, "Input should be greater than or equal to -180"),
@ -97,11 +78,12 @@ def test_prediction_common_settings_valid(
def test_prediction_common_settings_invalid(field_name, invalid_value, expected_error):
"""Test invalid settings for PredictionCommonSettings."""
valid_data = {
"prediction_hours": 48,
"prediction_historic_hours": 24,
"hours": 48,
"historic_hours": 24,
"latitude": 40.7128,
"longitude": -74.0060,
}
assert PredictionCommonSettings(**valid_data) is not None
valid_data[field_name] = invalid_value
with pytest.raises(ValidationError, match=expected_error):
@ -110,16 +92,14 @@ def test_prediction_common_settings_invalid(field_name, invalid_value, expected_
def test_prediction_common_settings_no_location():
"""Test that timezone is None when latitude and longitude are not provided."""
settings = PredictionCommonSettings(
prediction_hours=48, prediction_historic_hours=24, latitude=None, longitude=None
)
settings = PredictionCommonSettings(hours=48, historic_hours=24, latitude=None, longitude=None)
assert settings.timezone is None
def test_prediction_common_settings_with_location():
"""Test that timezone is correctly computed when latitude and longitude are provided."""
settings = PredictionCommonSettings(
prediction_hours=48, prediction_historic_hours=24, latitude=34.0522, longitude=-118.2437
hours=48, historic_hours=24, latitude=34.0522, longitude=-118.2437
)
assert settings.timezone == "America/Los_Angeles"

View File

@ -101,14 +101,14 @@ class TestPredictionBase:
assert base.config.prediction.latitude == 2.5
def test_config_value_from_field_default(self, base, monkeypatch):
assert base.config.prediction.model_fields["prediction_hours"].default == 48
assert base.config.prediction.prediction_hours == 48
monkeypatch.setenv("EOS_PREDICTION__PREDICTION_HOURS", "128")
assert base.config.prediction.model_fields["hours"].default == 48
assert base.config.prediction.hours == 48
monkeypatch.setenv("EOS_PREDICTION__HOURS", "128")
base.config.reset_settings()
assert base.config.prediction.prediction_hours == 128
monkeypatch.delenv("EOS_PREDICTION__PREDICTION_HOURS")
assert base.config.prediction.hours == 128
monkeypatch.delenv("EOS_PREDICTION__HOURS")
base.config.reset_settings()
assert base.config.prediction.prediction_hours == 48
assert base.config.prediction.hours == 48
def test_get_config_value_key_error(self, base):
with pytest.raises(AttributeError):
@ -159,14 +159,14 @@ class TestPredictionProvider:
"""Test that computed fields `end_datetime` and `keep_datetime` are correctly calculated."""
ems_eos = get_ems()
ems_eos.set_start_datetime(sample_start_datetime)
provider.config.prediction.prediction_hours = 24 # 24 hours into the future
provider.config.prediction.prediction_historic_hours = 48 # 48 hours into the past
provider.config.prediction.hours = 24 # 24 hours into the future
provider.config.prediction.historic_hours = 48 # 48 hours into the past
expected_end_datetime = sample_start_datetime + to_duration(
provider.config.prediction.prediction_hours * 3600
provider.config.prediction.hours * 3600
)
expected_keep_datetime = sample_start_datetime - to_duration(
provider.config.prediction.prediction_historic_hours * 3600
provider.config.prediction.historic_hours * 3600
)
assert (
@ -183,8 +183,8 @@ class TestPredictionProvider:
# EOS config supersedes
ems_eos = get_ems()
# The following values are currently not set in EOS config, we can override
monkeypatch.setenv("EOS_PREDICTION__PREDICTION_HISTORIC_HOURS", "2")
assert os.getenv("EOS_PREDICTION__PREDICTION_HISTORIC_HOURS") == "2"
monkeypatch.setenv("EOS_PREDICTION__HISTORIC_HOURS", "2")
assert os.getenv("EOS_PREDICTION__HISTORIC_HOURS") == "2"
monkeypatch.setenv("EOS_PREDICTION__LATITUDE", "37.7749")
assert os.getenv("EOS_PREDICTION__LATITUDE") == "37.7749"
monkeypatch.setenv("EOS_PREDICTION__LONGITUDE", "-122.4194")
@ -194,13 +194,13 @@ class TestPredictionProvider:
ems_eos.set_start_datetime(sample_start_datetime)
provider.update_data()
assert provider.config.prediction.prediction_hours == config_eos.prediction.prediction_hours
assert provider.config.prediction.prediction_historic_hours == 2
assert provider.config.prediction.hours == config_eos.prediction.hours
assert provider.config.prediction.historic_hours == 2
assert provider.config.prediction.latitude == 37.7749
assert provider.config.prediction.longitude == -122.4194
assert provider.start_datetime == sample_start_datetime
assert provider.end_datetime == sample_start_datetime + to_duration(
f"{provider.config.prediction.prediction_hours} hours"
f"{provider.config.prediction.hours} hours"
)
assert provider.keep_datetime == sample_start_datetime - to_duration("2 hours")
@ -290,7 +290,7 @@ class TestPredictionContainer:
ems_eos.set_start_datetime(to_datetime(start, in_timezone="Europe/Berlin"))
settings = {
"prediction": {
"prediction_hours": hours,
"hours": hours,
}
}
container.config.merge_settings_from_dict(settings)
@ -320,7 +320,7 @@ class TestPredictionContainer:
ems_eos.set_start_datetime(to_datetime(start, in_timezone="Europe/Berlin"))
settings = {
"prediction": {
"prediction_historic_hours": historic_hours,
"historic_hours": historic_hours,
}
}
container.config.merge_settings_from_dict(settings)
@ -328,7 +328,7 @@ class TestPredictionContainer:
assert compare_datetimes(container.keep_datetime, expected).equal
@pytest.mark.parametrize(
"start, prediction_hours, expected_hours",
"start, hours, expected_hours",
[
("2024-11-10 00:00:00", 24, 24), # No DST in Germany
("2024-08-10 00:00:00", 24, 24), # DST in Germany
@ -336,13 +336,13 @@ class TestPredictionContainer:
("2024-10-27 00:00:00", 24, 25), # DST change in Germany (25 hours/ day)
],
)
def test_total_hours(self, container, start, prediction_hours, expected_hours):
def test_total_hours(self, container, start, hours, expected_hours):
"""Test the `total_hours` property."""
ems_eos = get_ems()
ems_eos.set_start_datetime(to_datetime(start, in_timezone="Europe/Berlin"))
settings = {
"prediction": {
"prediction_hours": prediction_hours,
"hours": hours,
}
}
container.config.merge_settings_from_dict(settings)
@ -363,7 +363,7 @@ class TestPredictionContainer:
ems_eos.set_start_datetime(to_datetime(start, in_timezone="Europe/Berlin"))
settings = {
"prediction": {
"prediction_historic_hours": historic_hours,
"historic_hours": historic_hours,
}
}
container.config.merge_settings_from_dict(settings)

View File

@ -26,13 +26,13 @@ def sample_settings(config_eos):
"""Fixture that adds settings data to the global config."""
settings = {
"prediction": {
"prediction_hours": 48,
"prediction_historic_hours": 24,
"hours": 48,
"historic_hours": 24,
"latitude": 52.52,
"longitude": 13.405,
},
"pvforecast": {
"pvforecast_provider": "PVForecastAkkudoktor",
"provider": "PVForecastAkkudoktor",
"pvforecast0_peakpower": 5.0,
"pvforecast0_surface_azimuth": -10,
"pvforecast0_surface_tilt": 7,
@ -59,7 +59,7 @@ def sample_settings(config_eos):
# Merge settings to config
config_eos.merge_settings_from_dict(settings)
assert config_eos.pvforecast.pvforecast_provider == "PVForecastAkkudoktor"
assert config_eos.pvforecast.provider == "PVForecastAkkudoktor"
return config_eos
@ -147,13 +147,13 @@ sample_value = AkkudoktorForecastValue(
)
sample_config_data = {
"prediction": {
"prediction_hours": 48,
"prediction_historic_hours": 24,
"hours": 48,
"historic_hours": 24,
"latitude": 52.52,
"longitude": 13.405,
},
"pvforecast": {
"pvforecast_provider": "PVForecastAkkudoktor",
"provider": "PVForecastAkkudoktor",
"pvforecast0_peakpower": 5.0,
"pvforecast0_surface_azimuth": 180,
"pvforecast0_surface_tilt": 30,

View File

@ -13,11 +13,11 @@ FILE_TESTDATA_PVFORECASTIMPORT_1_JSON = DIR_TESTDATA.joinpath("import_input_1.js
@pytest.fixture
def pvforecast_provider(sample_import_1_json, config_eos):
def provider(sample_import_1_json, config_eos):
"""Fixture to create a PVForecastProvider instance."""
settings = {
"pvforecast": {
"pvforecast_provider": "PVForecastImport",
"provider": "PVForecastImport",
"provider_settings": {
"pvforecastimport_file_path": str(FILE_TESTDATA_PVFORECASTIMPORT_1_JSON),
"pvforecastimport_json": json.dumps(sample_import_1_json),
@ -43,24 +43,24 @@ def sample_import_1_json():
# ------------------------------------------------
def test_singleton_instance(pvforecast_provider):
def test_singleton_instance(provider):
"""Test that PVForecastForecast behaves as a singleton."""
another_instance = PVForecastImport()
assert pvforecast_provider is another_instance
assert provider is another_instance
def test_invalid_provider(pvforecast_provider, config_eos):
"""Test requesting an unsupported pvforecast_provider."""
def test_invalid_provider(provider, config_eos):
"""Test requesting an unsupported provider."""
settings = {
"pvforecast": {
"pvforecast_provider": "<invalid>",
"provider": "<invalid>",
"provider_settings": {
"pvforecastimport_file_path": str(FILE_TESTDATA_PVFORECASTIMPORT_1_JSON),
},
}
}
config_eos.merge_settings_from_dict(settings)
assert not pvforecast_provider.enabled()
assert not provider.enabled()
# ------------------------------------------------
@ -81,7 +81,7 @@ def test_invalid_provider(pvforecast_provider, config_eos):
("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, config_eos):
def test_import(provider, sample_import_1_json, start_datetime, from_file, config_eos):
"""Test fetching forecast from import."""
ems_eos = get_ems()
ems_eos.set_start_datetime(to_datetime(start_datetime, in_timezone="Europe/Berlin"))
@ -91,25 +91,23 @@ def test_import(pvforecast_provider, sample_import_1_json, start_datetime, from_
else:
config_eos.pvforecast.provider_settings.pvforecastimport_file_path = None
assert config_eos.pvforecast.provider_settings.pvforecastimport_file_path is None
pvforecast_provider.clear()
provider.clear()
# Call the method
pvforecast_provider.update_data()
provider.update_data()
# Assert: Verify the result is as expected
assert pvforecast_provider.start_datetime is not None
assert pvforecast_provider.total_hours is not None
assert compare_datetimes(pvforecast_provider.start_datetime, ems_eos.start_datetime).equal
assert provider.start_datetime is not None
assert provider.total_hours is not None
assert compare_datetimes(provider.start_datetime, ems_eos.start_datetime).equal
values = sample_import_1_json["pvforecast_ac_power"]
value_datetime_mapping = pvforecast_provider.import_datetimes(
ems_eos.start_datetime, len(values)
)
value_datetime_mapping = provider.import_datetimes(ems_eos.start_datetime, len(values))
for i, mapping in enumerate(value_datetime_mapping):
assert i < len(pvforecast_provider.records)
assert i < len(provider.records)
expected_datetime, expected_value_index = mapping
expected_value = values[expected_value_index]
result_datetime = pvforecast_provider.records[i].date_time
result_value = pvforecast_provider.records[i]["pvforecast_ac_power"]
result_datetime = provider.records[i].date_time
result_value = provider.records[i]["pvforecast_ac_power"]
# print(f"{i}: Expected: {expected_datetime}:{expected_value}")
# print(f"{i}: Result: {result_datetime}:{result_value}")

View File

@ -17,7 +17,7 @@ FILE_TESTDATA_WEATHERBRIGHTSKY_2_JSON = DIR_TESTDATA.joinpath("weatherforecast_b
@pytest.fixture
def weather_provider(monkeypatch):
def provider(monkeypatch):
"""Fixture to create a WeatherProvider instance."""
monkeypatch.setenv("EOS_WEATHER__WEATHER_PROVIDER", "BrightSky")
monkeypatch.setenv("EOS_PREDICTION__LATITUDE", "50.0")
@ -52,27 +52,27 @@ def cache_store():
# ------------------------------------------------
def test_singleton_instance(weather_provider):
def test_singleton_instance(provider):
"""Test that WeatherForecast behaves as a singleton."""
another_instance = WeatherBrightSky()
assert weather_provider is another_instance
assert provider is another_instance
def test_invalid_provider(weather_provider, monkeypatch):
"""Test requesting an unsupported weather_provider."""
def test_invalid_provider(provider, monkeypatch):
"""Test requesting an unsupported provider."""
monkeypatch.setenv("EOS_WEATHER__WEATHER_PROVIDER", "<invalid>")
weather_provider.config.reset_settings()
assert not weather_provider.enabled()
provider.config.reset_settings()
assert not provider.enabled()
def test_invalid_coordinates(weather_provider, monkeypatch):
def test_invalid_coordinates(provider, monkeypatch):
"""Test invalid coordinates raise ValueError."""
monkeypatch.setenv("EOS_PREDICTION__LATITUDE", "1000")
monkeypatch.setenv("EOS_PREDICTION__LONGITUDE", "1000")
with pytest.raises(
ValueError, # match="Latitude '1000' and/ or longitude `1000` out of valid range."
):
weather_provider.config.reset_settings()
provider.config.reset_settings()
# ------------------------------------------------
@ -80,15 +80,13 @@ def test_invalid_coordinates(weather_provider, monkeypatch):
# ------------------------------------------------
def test_irridiance_estimate_from_cloud_cover(weather_provider):
def test_irridiance_estimate_from_cloud_cover(provider):
"""Test cloud cover to irradiance estimation."""
cloud_cover_data = pd.Series(
data=[20, 50, 80], index=pd.date_range("2023-10-22", periods=3, freq="h")
)
ghi, dni, dhi = weather_provider.estimate_irradiance_from_cloud_cover(
50.0, 10.0, cloud_cover_data
)
ghi, dni, dhi = provider.estimate_irradiance_from_cloud_cover(50.0, 10.0, cloud_cover_data)
assert ghi == [0, 0, 0]
assert dhi == [0, 0, 0]
@ -101,7 +99,7 @@ def test_irridiance_estimate_from_cloud_cover(weather_provider):
@patch("requests.get")
def test_request_forecast(mock_get, weather_provider, sample_brightsky_1_json):
def test_request_forecast(mock_get, provider, sample_brightsky_1_json):
"""Test requesting forecast from BrightSky."""
# Mock response object
mock_response = Mock()
@ -110,10 +108,10 @@ def test_request_forecast(mock_get, weather_provider, sample_brightsky_1_json):
mock_get.return_value = mock_response
# Preset, as this is usually done by update()
weather_provider.config.update()
provider.config.update()
# Test function
brightsky_data = weather_provider._request_forecast()
brightsky_data = provider._request_forecast()
assert isinstance(brightsky_data, dict)
assert brightsky_data["weather"][0] == {
@ -150,7 +148,7 @@ def test_request_forecast(mock_get, weather_provider, sample_brightsky_1_json):
@patch("requests.get")
def test_update_data(mock_get, weather_provider, sample_brightsky_1_json, cache_store):
def test_update_data(mock_get, provider, sample_brightsky_1_json, cache_store):
"""Test fetching forecast from BrightSky."""
# Mock response object
mock_response = Mock()
@ -163,14 +161,14 @@ def test_update_data(mock_get, weather_provider, sample_brightsky_1_json, cache_
# Call the method
ems_eos = get_ems()
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)
provider.update_data(force_enable=True, force_update=True)
# Assert: Verify the result is as expected
mock_get.assert_called_once()
assert len(weather_provider) == 338
assert len(provider) == 338
# with open(FILE_TESTDATA_WEATHERBRIGHTSKY_2_JSON, "w") as f_out:
# f_out.write(weather_provider.to_json())
# f_out.write(provider.to_json())
# ------------------------------------------------
@ -179,14 +177,14 @@ def test_update_data(mock_get, weather_provider, sample_brightsky_1_json, cache_
@pytest.mark.skip(reason="For development only")
def test_brightsky_development_forecast_data(weather_provider):
def test_brightsky_development_forecast_data(provider):
"""Fetch data from real BrightSky server."""
# Preset, as this is usually done by update_data()
weather_provider.start_datetime = to_datetime("2024-10-26 00:00:00")
weather_provider.latitude = 50.0
weather_provider.longitude = 10.0
provider.start_datetime = to_datetime("2024-10-26 00:00:00")
provider.latitude = 50.0
provider.longitude = 10.0
brightsky_data = weather_provider._request_forecast()
brightsky_data = provider._request_forecast()
with open(FILE_TESTDATA_WEATHERBRIGHTSKY_1_JSON, "w") as f_out:
json.dump(brightsky_data, f_out, indent=4)

View File

@ -21,11 +21,11 @@ FILE_TESTDATA_WEATHERCLEAROUTSIDE_1_DATA = DIR_TESTDATA.joinpath("weatherforecas
@pytest.fixture
def weather_provider(config_eos):
def provider(config_eos):
"""Fixture to create a WeatherProvider instance."""
settings = {
"weather": {
"weather_provider": "ClearOutside",
"provider": "ClearOutside",
},
"prediction": {
"latitude": 50.0,
@ -64,28 +64,28 @@ def cache_store():
# ------------------------------------------------
def test_singleton_instance(weather_provider):
def test_singleton_instance(provider):
"""Test that WeatherForecast behaves as a singleton."""
another_instance = WeatherClearOutside()
assert weather_provider is another_instance
assert provider is another_instance
def test_invalid_provider(weather_provider, config_eos):
"""Test requesting an unsupported weather_provider."""
def test_invalid_provider(provider, config_eos):
"""Test requesting an unsupported provider."""
settings = {
"weather": {
"weather_provider": "<invalid>",
"provider": "<invalid>",
}
}
config_eos.merge_settings_from_dict(settings)
assert not weather_provider.enabled()
assert not provider.enabled()
def test_invalid_coordinates(weather_provider, config_eos):
def test_invalid_coordinates(provider, config_eos):
"""Test invalid coordinates raise ValueError."""
settings = {
"weather": {
"weather_provider": "ClearOutside",
"provider": "ClearOutside",
},
"prediction": {
"latitude": 1000.0,
@ -103,15 +103,13 @@ def test_invalid_coordinates(weather_provider, config_eos):
# ------------------------------------------------
def test_irridiance_estimate_from_cloud_cover(weather_provider):
def test_irridiance_estimate_from_cloud_cover(provider):
"""Test cloud cover to irradiance estimation."""
cloud_cover_data = pd.Series(
data=[20, 50, 80], index=pd.date_range("2023-10-22", periods=3, freq="h")
)
ghi, dni, dhi = weather_provider.estimate_irradiance_from_cloud_cover(
50.0, 10.0, cloud_cover_data
)
ghi, dni, dhi = provider.estimate_irradiance_from_cloud_cover(50.0, 10.0, cloud_cover_data)
assert ghi == [0, 0, 0]
assert dhi == [0, 0, 0]
@ -124,7 +122,7 @@ def test_irridiance_estimate_from_cloud_cover(weather_provider):
@patch("requests.get")
def test_request_forecast(mock_get, weather_provider, sample_clearout_1_html, config_eos):
def test_request_forecast(mock_get, provider, sample_clearout_1_html, config_eos):
"""Test fetching forecast from ClearOutside."""
# Mock response object
mock_response = Mock()
@ -136,14 +134,14 @@ def test_request_forecast(mock_get, weather_provider, sample_clearout_1_html, co
config_eos.update()
# Test function
response = weather_provider._request_forecast()
response = provider._request_forecast()
assert response.status_code == 200
assert response.content == sample_clearout_1_html
@patch("requests.get")
def test_update_data(mock_get, weather_provider, sample_clearout_1_html, sample_clearout_1_data):
def test_update_data(mock_get, provider, sample_clearout_1_html, sample_clearout_1_data):
# Mock response object
mock_response = Mock()
mock_response.status_code = 200
@ -157,17 +155,17 @@ def test_update_data(mock_get, weather_provider, sample_clearout_1_html, sample_
# Call the method
ems_eos = get_ems()
ems_eos.set_start_datetime(expected_start)
weather_provider.update_data()
provider.update_data()
# Check for correct prediction time window
assert weather_provider.config.prediction.prediction_hours == 48
assert weather_provider.config.prediction.prediction_historic_hours == 48
assert compare_datetimes(weather_provider.start_datetime, expected_start).equal
assert compare_datetimes(weather_provider.end_datetime, expected_end).equal
assert compare_datetimes(weather_provider.keep_datetime, expected_keep).equal
assert provider.config.prediction.hours == 48
assert provider.config.prediction.historic_hours == 48
assert compare_datetimes(provider.start_datetime, expected_start).equal
assert compare_datetimes(provider.end_datetime, expected_end).equal
assert compare_datetimes(provider.keep_datetime, expected_keep).equal
# Verify the data
assert len(weather_provider) == 165 # 6 days, 24 hours per day - 7th day 21 hours
assert len(provider) == 165 # 6 days, 24 hours per day - 7th day 21 hours
# Check that specific values match the expected output
# for i, record in enumerate(weather_data.records):
@ -179,7 +177,7 @@ def test_update_data(mock_get, weather_provider, sample_clearout_1_html, sample_
@pytest.mark.skip(reason="Test fixture to be improved")
@patch("requests.get")
def test_cache_forecast(mock_get, weather_provider, sample_clearout_1_html, cache_store):
def test_cache_forecast(mock_get, provider, sample_clearout_1_html, cache_store):
"""Test that ClearOutside forecast data is cached with TTL.
This can not be tested with mock_get. Mock objects are not pickable and therefor can not be
@ -193,12 +191,12 @@ def test_cache_forecast(mock_get, weather_provider, sample_clearout_1_html, cach
cache_store.clear(clear_all=True)
weather_provider.update_data()
provider.update_data()
mock_get.assert_called_once()
forecast_data_first = weather_provider.to_json()
forecast_data_first = provider.to_json()
weather_provider.update_data()
forecast_data_second = weather_provider.to_json()
provider.update_data()
forecast_data_second = provider.to_json()
# Verify that cache returns the same object without calling the method again
assert forecast_data_first == forecast_data_second
# A mock object is not pickable and therefor can not be chached to file
@ -212,7 +210,7 @@ def test_cache_forecast(mock_get, weather_provider, sample_clearout_1_html, cach
@pytest.mark.skip(reason="For development only")
@patch("requests.get")
def test_development_forecast_data(mock_get, weather_provider, sample_clearout_1_html):
def test_development_forecast_data(mock_get, provider, sample_clearout_1_html):
# Mock response object
mock_response = Mock()
mock_response.status_code = 200
@ -220,14 +218,14 @@ def test_development_forecast_data(mock_get, weather_provider, sample_clearout_1
mock_get.return_value = mock_response
# Fill the instance
weather_provider.update_data(force_enable=True)
provider.update_data(force_enable=True)
with open(FILE_TESTDATA_WEATHERCLEAROUTSIDE_1_DATA, "w", encoding="utf8") as f_out:
f_out.write(weather_provider.to_json())
f_out.write(provider.to_json())
@pytest.mark.skip(reason="For development only")
def test_clearoutsides_development_scraper(weather_provider, sample_clearout_1_html):
def test_clearoutsides_development_scraper(provider, sample_clearout_1_html):
"""Test scraping from ClearOutside."""
soup = BeautifulSoup(sample_clearout_1_html, "html.parser")

View File

@ -13,14 +13,14 @@ FILE_TESTDATA_WEATHERIMPORT_1_JSON = DIR_TESTDATA.joinpath("import_input_1.json"
@pytest.fixture
def weather_provider(sample_import_1_json, config_eos):
def provider(sample_import_1_json, config_eos):
"""Fixture to create a WeatherProvider instance."""
settings = {
"weather": {
"weather_provider": "WeatherImport",
"provider": "WeatherImport",
"provider_settings": {
"weatherimport_file_path": str(FILE_TESTDATA_WEATHERIMPORT_1_JSON),
"weatherimport_json": json.dumps(sample_import_1_json),
"import_file_path": str(FILE_TESTDATA_WEATHERIMPORT_1_JSON),
"import_json": json.dumps(sample_import_1_json),
},
}
}
@ -43,24 +43,24 @@ def sample_import_1_json():
# ------------------------------------------------
def test_singleton_instance(weather_provider):
def test_singleton_instance(provider):
"""Test that WeatherForecast behaves as a singleton."""
another_instance = WeatherImport()
assert weather_provider is another_instance
assert provider is another_instance
def test_invalid_provider(weather_provider, config_eos, monkeypatch):
"""Test requesting an unsupported weather_provider."""
def test_invalid_provider(provider, config_eos, monkeypatch):
"""Test requesting an unsupported provider."""
settings = {
"weather": {
"weather_provider": "<invalid>",
"provider": "<invalid>",
"provider_settings": {
"weatherimport_file_path": str(FILE_TESTDATA_WEATHERIMPORT_1_JSON),
"import_file_path": str(FILE_TESTDATA_WEATHERIMPORT_1_JSON),
},
}
}
config_eos.merge_settings_from_dict(settings)
assert weather_provider.enabled() == False
assert provider.enabled() == False
# ------------------------------------------------
@ -81,33 +81,33 @@ def test_invalid_provider(weather_provider, config_eos, monkeypatch):
("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, config_eos):
def test_import(provider, sample_import_1_json, start_datetime, from_file, config_eos):
"""Test fetching forecast from Import."""
ems_eos = get_ems()
ems_eos.set_start_datetime(to_datetime(start_datetime, in_timezone="Europe/Berlin"))
if from_file:
config_eos.weather.provider_settings.weatherimport_json = None
assert config_eos.weather.provider_settings.weatherimport_json is None
config_eos.weather.provider_settings.import_json = None
assert config_eos.weather.provider_settings.import_json is None
else:
config_eos.weather.provider_settings.weatherimport_file_path = None
assert config_eos.weather.provider_settings.weatherimport_file_path is None
weather_provider.clear()
config_eos.weather.provider_settings.import_file_path = None
assert config_eos.weather.provider_settings.import_file_path is None
provider.clear()
# Call the method
weather_provider.update_data()
provider.update_data()
# Assert: Verify the result is as expected
assert weather_provider.start_datetime is not None
assert weather_provider.total_hours is not None
assert compare_datetimes(weather_provider.start_datetime, ems_eos.start_datetime).equal
assert provider.start_datetime is not None
assert provider.total_hours is not None
assert compare_datetimes(provider.start_datetime, ems_eos.start_datetime).equal
values = sample_import_1_json["weather_temp_air"]
value_datetime_mapping = weather_provider.import_datetimes(ems_eos.start_datetime, len(values))
value_datetime_mapping = provider.import_datetimes(ems_eos.start_datetime, len(values))
for i, mapping in enumerate(value_datetime_mapping):
assert i < len(weather_provider.records)
assert i < len(provider.records)
expected_datetime, expected_value_index = mapping
expected_value = values[expected_value_index]
result_datetime = weather_provider.records[i].date_time
result_value = weather_provider.records[i]["weather_temp_air"]
result_datetime = provider.records[i].date_time
result_value = provider.records[i]["weather_temp_air"]
# print(f"{i}: Expected: {expected_datetime}:{expected_value}")
# print(f"{i}: Result: {result_datetime}:{result_value}")

View File

@ -35,7 +35,7 @@
"inverter": {
"device_id": "inverter1",
"max_power_wh": 10000,
"battery": "battery1"
"battery_id": "battery1"
},
"eauto": {
"device_id": "ev1",

View File

@ -173,7 +173,7 @@
"inverter": {
"device_id": "inverter1",
"max_power_wh": 10000,
"battery": "battery1"
"battery_id": "battery1"
},
"dishwasher": {
"device_id": "dishwasher1",