feat: make home assistant add-on run optimization by default (#940)

When running as Home Assistant add-on the only viable usage is running with
cyclic optimization. Make this the default to als propvide a better experience
for first time users. The optimization will start with demo data, which also
helps to configure Akkudoktor-EOS to the personal usage.

The disabling of the automatic energy management is now an explicit mode
"DISABLED".

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
This commit is contained in:
Bobby Noelte
2026-03-13 15:48:43 +01:00
committed by GitHub
parent 8a9aec6d57
commit b67148c47f
12 changed files with 81 additions and 44 deletions

View File

@@ -6,7 +6,7 @@
# the root directory (no add-on folder as usual). # the root directory (no add-on folder as usual).
name: "Akkudoktor-EOS" name: "Akkudoktor-EOS"
version: "0.2.0.dev2603130753300674" version: "0.2.0.dev2603131189846912"
slug: "eos" slug: "eos"
description: "Akkudoktor-EOS add-on" description: "Akkudoktor-EOS add-on"
url: "https://github.com/Akkudoktor-EOS/EOS" url: "https://github.com/Akkudoktor-EOS/EOS"

View File

@@ -8,7 +8,7 @@
| Name | Environment Variable | Type | Read-Only | Default | Description | | Name | Environment Variable | Type | Read-Only | Default | Description |
| ---- | -------------------- | ---- | --------- | ------- | ----------- | | ---- | -------------------- | ---- | --------- | ------- | ----------- |
| interval | `EOS_EMS__INTERVAL` | `float` | `rw` | `300.0` | Intervall between EOS energy management runs [seconds]. | | interval | `EOS_EMS__INTERVAL` | `float` | `rw` | `300.0` | Intervall between EOS energy management runs [seconds]. |
| mode | `EOS_EMS__MODE` | `Optional[akkudoktoreos.core.emsettings.EnergyManagementMode]` | `rw` | `None` | Energy management mode [OPTIMIZATION | PREDICTION]. | | mode | `EOS_EMS__MODE` | `<enum 'EnergyManagementMode'>` | `rw` | `required` | Energy management mode [DISABLED | OPTIMIZATION | PREDICTION]. |
| startup_delay | `EOS_EMS__STARTUP_DELAY` | `float` | `rw` | `5` | Startup delay in seconds for EOS energy management runs. | | startup_delay | `EOS_EMS__STARTUP_DELAY` | `float` | `rw` | `5` | Startup delay in seconds for EOS energy management runs. |
::: :::
<!-- pyml enable line-length --> <!-- pyml enable line-length -->

View File

@@ -1,6 +1,6 @@
# Akkudoktor-EOS # Akkudoktor-EOS
**Version**: `v0.2.0.dev2603130753300674` **Version**: `v0.2.0.dev2603131189846912`
<!-- pyml disable line-length --> <!-- pyml disable line-length -->
**Description**: This project provides a comprehensive solution for simulating and optimizing an energy system based on renewable energy sources. With a focus on photovoltaic (PV) systems, battery storage (batteries), load management (consumer requirements), heat pumps, electric vehicles, and consideration of electricity price data, this system enables forecasting and optimization of energy flow and costs over a specified period. **Description**: This project provides a comprehensive solution for simulating and optimizing an energy system based on renewable energy sources. With a focus on photovoltaic (PV) systems, battery storage (batteries), load management (consumer requirements), heat pumps, electric vehicles, and consideration of electricity price data, this system enables forecasting and optimization of energy flow and costs over a specified period.

View File

@@ -107,10 +107,11 @@ If no interval is configured (`None`, `null`) there will be only one energy mana
startup. startup.
::: :::
The energy management can be run in two modes: The energy management can be run in three modes:
- **OPTIMIZATION**: A full optimization is done. This includes update of predictions. - **OPTIMIZATION**: A full optimization is done. This includes update of predictions.
- **PREDICTION**: Only the predictions are updated. - **PREDICTION**: Only the predictions are updated.
- **DISABLED**: No energy management is run.
**Example:** **Example:**

View File

@@ -17,6 +17,12 @@ The `POST /optimize` endpoint interface does not regard configurations set for t
passed to the request. You have to set the parameters even if given in the configuration. passed to the request. You have to set the parameters even if given in the configuration.
::: :::
:::{admonition} Warning
:class: warning
To prevent automatic optimization from interfering with `POST /optimize` requests, set `ems.mode`
to `DISABLED` in the configuration.
:::
## Input Payload ## Input Payload
### Sample Request ### Sample Request
@@ -86,6 +92,12 @@ passed to the request. You have to set the parameters even if given in the confi
"initial_soc_percentage": 54, "initial_soc_percentage": 54,
"min_soc_percentage": 0 "min_soc_percentage": 0
}, },
"dishwasher": {
"device_id": "dishwasher1",
"consumption_wh": 2000,
"duration_h": 3,
"time_windows": null
},
"temperature_forecast": [ "temperature_forecast": [
18.3, 17.8, 16.9, 16.2, 15.6, 15.1, 14.6, 14.2, 14.3, 14.8, 15.7, 16.7, 17.4, 18.3, 17.8, 16.9, 16.2, 15.6, 15.1, 14.6, 14.2, 14.3, 14.8, 15.7, 16.7, 17.4,
18.0, 18.6, 19.2, 19.1, 18.7, 18.5, 17.7, 16.2, 14.6, 13.6, 13.0, 12.6, 12.2, 18.0, 18.6, 19.2, 19.1, 18.7, 18.5, 17.7, 16.2, 14.6, 13.6, 13.0, 12.6, 12.2,

View File

@@ -8,7 +8,7 @@
"name": "Apache 2.0", "name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html" "url": "https://www.apache.org/licenses/LICENSE-2.0.html"
}, },
"version": "v0.2.0.dev2603130753300674" "version": "v0.2.0.dev2603131189846912"
}, },
"paths": { "paths": {
"/v1/admin/cache/clear": { "/v1/admin/cache/clear": {
@@ -3833,15 +3833,8 @@
] ]
}, },
"mode": { "mode": {
"anyOf": [ "$ref": "#/components/schemas/EnergyManagementMode",
{ "description": "Energy management mode [DISABLED | OPTIMIZATION | PREDICTION].",
"$ref": "#/components/schemas/EnergyManagementMode"
},
{
"type": "null"
}
],
"description": "Energy management mode [OPTIMIZATION | PREDICTION].",
"examples": [ "examples": [
"OPTIMIZATION", "OPTIMIZATION",
"PREDICTION" "PREDICTION"
@@ -3855,6 +3848,7 @@
"EnergyManagementMode": { "EnergyManagementMode": {
"type": "string", "type": "string",
"enum": [ "enum": [
"DISABLED",
"PREDICTION", "PREDICTION",
"OPTIMIZATION" "OPTIMIZATION"
], ],
@@ -5410,7 +5404,7 @@
"examples": [ "examples": [
[ [
{ {
"duration": "2 hours", "duration": "3 hours",
"start_time": "10:00" "start_time": "10:00"
} }
] ]

View File

@@ -23,7 +23,7 @@ from pydantic import Field, computed_field, field_validator
# settings # settings
from akkudoktoreos.adapter.adapter import AdapterCommonSettings from akkudoktoreos.adapter.adapter import AdapterCommonSettings
from akkudoktoreos.config.configabc import SettingsBaseModel from akkudoktoreos.config.configabc import SettingsBaseModel, is_home_assistant_addon
from akkudoktoreos.config.configmigrate import migrate_config_data, migrate_config_file from akkudoktoreos.config.configmigrate import migrate_config_data, migrate_config_file
from akkudoktoreos.core.cachesettings import CacheCommonSettings from akkudoktoreos.core.cachesettings import CacheCommonSettings
from akkudoktoreos.core.coreabc import SingletonMixin from akkudoktoreos.core.coreabc import SingletonMixin
@@ -69,14 +69,6 @@ def get_absolute_path(
return None return None
def is_home_assistant_addon() -> bool:
"""Detect Home Assistant add-on environment.
Home Assistant sets this environment variable automatically.
"""
return "HASSIO_TOKEN" in os.environ or "SUPERVISOR_TOKEN" in os.environ
def default_data_folder_path() -> Path: def default_data_folder_path() -> Path:
"""Provide default data folder path. """Provide default data folder path.

View File

@@ -1,6 +1,7 @@
"""Abstract and base classes for configuration.""" """Abstract and base classes for configuration."""
import calendar import calendar
import os
from typing import Any, ClassVar, Iterator, Optional, Union from typing import Any, ClassVar, Iterator, Optional, Union
import numpy as np import numpy as np
@@ -18,6 +19,14 @@ from akkudoktoreos.utils.datetimeutil import (
) )
def is_home_assistant_addon() -> bool:
"""Detect Home Assistant add-on environment.
Home Assistant sets this environment variable automatically.
"""
return "HASSIO_TOKEN" in os.environ or "SUPERVISOR_TOKEN" in os.environ
class SettingsBaseModel(PydanticBaseModel): class SettingsBaseModel(PydanticBaseModel):
"""Base model class for all settings configurations.""" """Base model class for all settings configurations."""

View File

@@ -154,8 +154,8 @@ class EnergyManagement(
@classmethod @classmethod
def _run( def _run(
cls, cls,
start_datetime: Optional[DateTime] = None, start_datetime: DateTime,
mode: Optional[EnergyManagementMode] = None, mode: EnergyManagementMode,
genetic_parameters: Optional[GeneticOptimizationParameters] = None, genetic_parameters: Optional[GeneticOptimizationParameters] = None,
genetic_individuals: Optional[int] = None, genetic_individuals: Optional[int] = None,
genetic_seed: Optional[int] = None, genetic_seed: Optional[int] = None,
@@ -170,14 +170,11 @@ class EnergyManagement(
optimization depending on the selected mode or configuration. optimization depending on the selected mode or configuration.
Args: Args:
start_datetime (DateTime, optional): The starting timestamp start_datetime (DateTime): The starting timestamp of the energy management run.
of the energy management run. Defaults to the current datetime mode (EnergyManagementMode): The management mode to use. Must be one of:
if not provided.
mode (EnergyManagementMode, optional): The management mode to use. Must be one of:
- "OPTIMIZATION": Runs the optimization process. - "OPTIMIZATION": Runs the optimization process.
- "PREDICTION": Updates the forecast without optimization. - "PREDICTION": Updates the forecast without optimization.
- "DISABLED": Does not run.
Defaults to the mode defined in the current configuration.
genetic_parameters (GeneticOptimizationParameters, optional): The genetic_parameters (GeneticOptimizationParameters, optional): The
parameter set for the genetic algorithm. If not provided, it will parameter set for the genetic algorithm. If not provided, it will
be constructed based on the current configuration and predictions. be constructed based on the current configuration and predictions.
@@ -196,8 +193,10 @@ class EnergyManagement(
None None
""" """
# Ensure there is only one optimization/ energy management run at a time # Ensure there is only one optimization/ energy management run at a time
if mode not in (None, "PREDICTION", "OPTIMIZATION"): if not EnergyManagementMode.is_valid(mode):
raise ValueError(f"Unknown energy management mode {mode}.") raise ValueError(f"Unknown energy management mode {mode}.")
if mode == EnergyManagementMode.DISABLED:
return
logger.info("Starting energy management run.") logger.info("Starting energy management run.")
@@ -220,9 +219,7 @@ class EnergyManagement(
cls._stage = EnergyManagementStage.FORECAST_RETRIEVAL cls._stage = EnergyManagementStage.FORECAST_RETRIEVAL
if mode is None: if mode == EnergyManagementMode.PREDICTION:
mode = cls.config.ems.mode
if mode is None or mode == "PREDICTION":
# Update the predictions # Update the predictions
cls.prediction.update_data(force_enable=force_enable, force_update=force_update) cls.prediction.update_data(force_enable=force_enable, force_update=force_update)
logger.info("Energy management run done (predictions updated)") logger.info("Energy management run done (predictions updated)")
@@ -346,6 +343,10 @@ class EnergyManagement(
async with self._run_lock: async with self._run_lock:
loop = get_running_loop() loop = get_running_loop()
# Create a partial function with parameters "baked in" # Create a partial function with parameters "baked in"
if start_datetime is None:
start_datetime = to_datetime()
if mode is None:
mode = self.config.ems.mode
func = partial( func = partial(
EnergyManagement._run, EnergyManagement._run,
start_datetime=start_datetime, start_datetime=start_datetime,

View File

@@ -4,19 +4,47 @@ Kept in an extra module to avoid cyclic dependencies on package import.
""" """
from enum import Enum from enum import Enum
from typing import Optional from typing import Optional, Union
from pydantic import Field from pydantic import Field
from akkudoktoreos.config.configabc import SettingsBaseModel from akkudoktoreos.config.configabc import SettingsBaseModel, is_home_assistant_addon
class EnergyManagementMode(str, Enum): class EnergyManagementMode(str, Enum):
"""Energy management mode.""" """Energy management mode."""
DISABLED = "DISABLED"
PREDICTION = "PREDICTION" PREDICTION = "PREDICTION"
OPTIMIZATION = "OPTIMIZATION" OPTIMIZATION = "OPTIMIZATION"
@classmethod
def is_valid(cls, mode: Union[str, "EnergyManagementMode"]) -> bool:
"""Check if value is a valid mode."""
try:
cls(mode)
return True
except (ValueError, TypeError):
return False
@classmethod
def from_value(cls, value: str) -> Optional["EnergyManagementMode"]:
"""Safely convert string to enum, return None if invalid."""
try:
return cls(value)
except ValueError:
return None
def ems_default_mode() -> EnergyManagementMode:
"""Provide default EMS mode.
Returns OPTIMIZATION when running under Home Assistant, else DISABLED.
"""
if is_home_assistant_addon():
return EnergyManagementMode.OPTIMIZATION
return EnergyManagementMode.DISABLED
class EnergyManagementCommonSettings(SettingsBaseModel): class EnergyManagementCommonSettings(SettingsBaseModel):
"""Energy Management Configuration.""" """Energy Management Configuration."""
@@ -38,10 +66,10 @@ class EnergyManagementCommonSettings(SettingsBaseModel):
}, },
) )
mode: Optional[EnergyManagementMode] = Field( mode: EnergyManagementMode = Field(
default=None, default_factory=ems_default_mode,
json_schema_extra={ json_schema_extra={
"description": "Energy management mode [OPTIMIZATION | PREDICTION].", "description": "Energy management mode [DISABLED | OPTIMIZATION | PREDICTION].",
"examples": ["OPTIMIZATION", "PREDICTION"], "examples": ["OPTIMIZATION", "PREDICTION"],
}, },
) )

View File

@@ -139,7 +139,7 @@ class HomeApplianceParameters(DeviceParameters):
"description": "List of allowed time windows. Defaults to optimization general time window.", "description": "List of allowed time windows. Defaults to optimization general time window.",
"examples": [ "examples": [
[ [
{"start_time": "10:00", "duration": "2 hours"}, {"start_time": "10:00", "duration": "3 hours"},
], ],
], ],
}, },

View File

@@ -727,9 +727,9 @@ def Configuration(
# Home Assistant adapter optimization solution entities # Home Assistant adapter optimization solution entities
update_form_factory = make_config_update_list_form(eos_solution_entity_ids) update_form_factory = make_config_update_list_form(eos_solution_entity_ids)
elif config["name"].startswith("ems.mode"): elif config["name"].startswith("ems.mode"):
# Energy managemnt mode # Energy management mode
update_form_factory = make_config_update_value_form( update_form_factory = make_config_update_value_form(
["OPTIMIZATION", "PREDICTION", "None"] ["OPTIMIZATION", "PREDICTION", "DISABLED"]
) )
elif config["name"].endswith("elecpricefixed.time_windows.windows"): elif config["name"].endswith("elecpricefixed.time_windows.windows"):
update_form_factory = make_config_update_time_windows_windows_form( update_form_factory = make_config_update_time_windows_windows_form(