chore: improve error msg on home assistant add-on port config (#1186)

When running as a Home Assistant add-on the ports shall not be changed
as this would break config.yaml that is used by Home Assistant. Prevent
the change and return an error message.

Extra fixes:

* fix: EOS configuration initialisation by cli under EOSdash.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
This commit is contained in:
Bobby Noelte
2026-07-23 15:11:18 +02:00
committed by GitHub
parent 252d4b6123
commit 6093d8d348
15 changed files with 133 additions and 86 deletions

2
.env
View File

@@ -11,7 +11,7 @@ DOCKER_COMPOSE_DATA_DIR=${HOME}/.local/share/net.akkudoktor.eos
# -----------------------------------------------------------------------------
# Image / build
# -----------------------------------------------------------------------------
VERSION=0.3.0.dev2607201457168588
VERSION=0.3.0.dev2607231185985809
PYTHON_VERSION=3.13.9
# -----------------------------------------------------------------------------

View File

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

View File

@@ -7,11 +7,11 @@
| Name | Environment Variable | Type | Read-Only | Default | Description |
| ---- | -------------------- | ---- | --------- | ------- | ----------- |
| eosdash_host | `EOS_SERVER__EOSDASH_HOST` | `Optional[str]` | `rw` | `None` | EOSdash server IP address. Defaults to EOS server IP address. |
| eosdash_port | `EOS_SERVER__EOSDASH_PORT` | `Optional[int]` | `rw` | `None` | EOSdash server IP port number. Defaults to EOS server IP port number + 1. |
| eosdash_host | `EOS_SERVER__EOSDASH_HOST` | `str` | `rw` | `127.0.0.1` | EOSdash server IP address. Defaults to EOS server IP address. |
| eosdash_port | `EOS_SERVER__EOSDASH_PORT` | `int` | `rw` | `8504` | EOSdash server IP port number. Defaults to 8504. |
| eosdash_supervise_interval_sec | `EOS_SERVER__EOSDASH_SUPERVISE_INTERVAL_SEC` | `int` | `rw` | `10` | Supervision interval for EOS server to supervise EOSdash [seconds]. |
| host | `EOS_SERVER__HOST` | `Optional[str]` | `rw` | `127.0.0.1` | EOS server IP address. Defaults to 127.0.0.1. |
| port | `EOS_SERVER__PORT` | `Optional[int]` | `rw` | `8503` | EOS server IP port number. Defaults to 8503. |
| host | `EOS_SERVER__HOST` | `str` | `rw` | `127.0.0.1` | EOS server IP address. Defaults to 127.0.0.1. |
| port | `EOS_SERVER__PORT` | `int` | `rw` | `8503` | EOS server IP port number. Defaults to 8503. |
| reload | `EOS_SERVER__RELOAD` | `Optional[bool]` | `rw` | `False` | Enable server auto-reload for debugging or development. Default is False. Monitors the package directory for changes and reloads the server. |
| run_as_user | `EOS_SERVER__RUN_AS_USER` | `Optional[str]` | `rw` | `None` | The name of the target user to switch to. If ``None`` (default), the current effective user is used and no privilege change is attempted. |
| startup_eosdash | `EOS_SERVER__STARTUP_EOSDASH` | `Optional[bool]` | `rw` | `True` | EOS server to start EOSdash server. Defaults to True. |

View File

@@ -1,6 +1,6 @@
# Akkudoktor-EOS
**Version**: `v0.3.0.dev2607210805856856`
**Version**: `v0.3.0.dev2607231185985809`
<!-- 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.

View File

@@ -8,7 +8,7 @@
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "v0.3.0.dev2607210805856856"
"version": "v0.3.0.dev2607231185985809"
},
"paths": {
"/v1/admin/cache/clear": {
@@ -8573,14 +8573,7 @@
"ServerCommonSettings": {
"properties": {
"host": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"type": "string",
"title": "Host",
"description": "EOS server IP address. Defaults to 127.0.0.1.",
"default": "127.0.0.1",
@@ -8590,14 +8583,7 @@
]
},
"port": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"type": "integer",
"title": "Port",
"description": "EOS server IP port number. Defaults to 8503.",
"default": 8503,
@@ -8632,32 +8618,20 @@
"default": true
},
"eosdash_host": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"type": "string",
"title": "Eosdash Host",
"description": "EOSdash server IP address. Defaults to EOS server IP address.",
"default": "127.0.0.1",
"examples": [
"127.0.0.1",
"localhost"
]
},
"eosdash_port": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"type": "integer",
"title": "Eosdash Port",
"description": "EOSdash server IP port number. Defaults to EOS server IP port number + 1.",
"description": "EOSdash server IP port number. Defaults to 8504.",
"default": 8504,
"examples": [
8504
]

View File

@@ -464,11 +464,21 @@ class ConfigEOS(SingletonMixin, SettingsEOSDefaults):
# Server host
if args.host is not None:
if "dash" in sys.argv[0]:
# Config is read from EOSdash
set_nested(settings, "server.eosdash_host", args.host)
logger.debug(f"CLI arg: server.eosdash_host set to {args.host}")
else:
set_nested(settings, "server.host", args.host)
logger.debug(f"CLI arg: server.host set to {args.host}")
# Server port
if args.port is not None:
if "dash" in sys.argv[0]:
# Config is read from EOSdash
set_nested(settings, "server.eosdash_port", args.port)
logger.debug(f"CLI arg: server.eosdash_port set to {args.port}")
else:
set_nested(settings, "server.port", args.port)
logger.debug(f"CLI arg: server.port set to {args.port}")

View File

@@ -39,6 +39,9 @@ from akkudoktoreos.server.server import (
)
from akkudoktoreos.utils.stringutil import str2bool
# Config is the EOS configuration
# Config uses sysv args for initialisation
# sysv host and port is set for EOSdash but expected to be set for EOS
config_eos = get_config(init=True)

View File

@@ -7,13 +7,13 @@ import pwd
import re
import socket
import time
from typing import Optional
from typing import Any, Optional
import psutil
from loguru import logger
from pydantic import Field, field_validator
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.config.configabc import SettingsBaseModel, is_home_assistant_addon
from akkudoktoreos.core.coreabc import get_config
@@ -22,6 +22,11 @@ def get_default_host() -> str:
return "127.0.0.1"
def get_default_port() -> int:
"""Default port for EOS."""
return 8503
def get_host_ip() -> str:
"""IP address of the host machine.
@@ -326,15 +331,15 @@ def fix_data_directories_permissions(run_as_user: Optional[str] = None) -> None:
class ServerCommonSettings(SettingsBaseModel):
"""Server Configuration."""
host: Optional[str] = Field(
host: str = Field(
default=get_default_host(),
json_schema_extra={
"description": "EOS server IP address. Defaults to 127.0.0.1.",
"examples": ["127.0.0.1", "localhost"],
},
)
port: Optional[int] = Field(
default=8503,
port: int = Field(
default=get_default_port(),
json_schema_extra={
"description": "EOS server IP port number. Defaults to 8503.",
"examples": [
@@ -349,17 +354,17 @@ class ServerCommonSettings(SettingsBaseModel):
default=True,
json_schema_extra={"description": "EOS server to start EOSdash server. Defaults to True."},
)
eosdash_host: Optional[str] = Field(
default=None,
eosdash_host: str = Field(
default=get_default_host(),
json_schema_extra={
"description": "EOSdash server IP address. Defaults to EOS server IP address.",
"examples": ["127.0.0.1", "localhost"],
},
)
eosdash_port: Optional[int] = Field(
default=None,
eosdash_port: int = Field(
default=get_default_port() + 1,
json_schema_extra={
"description": "EOSdash server IP port number. Defaults to EOS server IP port number + 1.",
"description": "EOSdash server IP port number. Defaults to 8504.",
"examples": [
8504,
],
@@ -406,12 +411,34 @@ class ServerCommonSettings(SettingsBaseModel):
value = validate_ip_or_hostname(value)
return value
@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):
@field_validator("port", mode="before")
def validate_server_port(cls, value: Any) -> int:
default_port = get_default_port()
if value is None:
return default_port
value = int(value)
if is_home_assistant_addon() and value != default_port:
raise ValueError(
f"Server port number `{default_port}` for Home Assistant add-on can not be changed."
)
if not (1024 <= value <= 49151):
raise ValueError("Server port number must be between 1024 and 49151.")
return value
@field_validator("eosdash_port", mode="before")
def validate_eosdash_port(cls, value: Any) -> int:
default_port = get_default_port() + 1
if value is None:
return default_port
value = int(value)
if is_home_assistant_addon() and value != default_port:
raise ValueError(
f"EOSdash port number `{default_port}` for Home Assistant add-on can not be changed."
)
if not (1024 <= value <= 49151):
raise ValueError("EOSdash port number must be between 1024 and 49151.")
return value
@field_validator("run_as_user")
def validate_user(cls, value: Optional[str]) -> Optional[str]:
if value is not None:

View File

@@ -80,7 +80,7 @@ def test_computed_paths(config_eos):
def test_config_from_env(monkeypatch, config_eos):
"""Test configuration from env."""
assert config_eos.server.port == 8503
assert config_eos.server.eosdash_port is None
assert config_eos.server.eosdash_port == 8504
monkeypatch.setenv("EOS_SERVER__PORT", "8553")
monkeypatch.setenv("EOS_SERVER__EOSDASH_PORT", "8555")

View File

@@ -11,9 +11,16 @@ import pytest
import requests
from conftest import cleanup_eos_eosdash
from loguru import logger
from pydantic import ValidationError
from akkudoktoreos.config.configabc import is_home_assistant_addon
from akkudoktoreos.core.version import __version__
from akkudoktoreos.server.server import get_default_host, wait_for_port_free
from akkudoktoreos.server.server import (
ServerCommonSettings,
get_default_host,
get_default_port,
wait_for_port_free,
)
class TestServer:
@@ -45,6 +52,38 @@ class TestServer:
assert str(data_ouput_path).startswith(eos_dir)
class TestServerSettingsValidation:
"""Test the port restrictions in ServerCommonSettings when running as HA addon."""
def test_ha_addon_default_ports_ok(self, config_eos, monkeypatch):
"""Default ports are accepted in HA addon mode."""
monkeypatch.setattr('akkudoktoreos.server.server.is_home_assistant_addon', lambda: True)
assert config_eos.server.port == get_default_port() # 8503
assert config_eos.server.eosdash_port == get_default_port() + 1 # 8504
def test_server_port_restriction_in_ha_addon(self, config_eos, monkeypatch):
"""Server port must be the default (8503) in HA addon mode."""
monkeypatch.setattr('akkudoktoreos.server.server.is_home_assistant_addon', lambda: True)
with pytest.raises(ValidationError) as excinfo:
config_eos.server.port = 9000
assert "Server port number `8503` for Home Assistant add-on can not be changed" in str(excinfo.value)
def test_eosdash_port_restriction_in_ha_addon(self, config_eos, monkeypatch):
"""EOSdash port must be the default (8504) in HA addon mode."""
monkeypatch.setattr('akkudoktoreos.server.server.is_home_assistant_addon', lambda: True)
with pytest.raises(ValidationError) as excinfo:
config_eos.server.eosdash_port = 9001
assert "EOSdash port number `8504` for Home Assistant add-on can not be changed" in str(excinfo.value)
def test_ports_allowed_when_not_ha_addon(self, config_eos):
"""Custom ports are allowed when not in HA addon mode."""
# is_home_assistant_addon() returns False by default in this test environment
config_eos.server.port = 9000
config_eos.server.eosdash_port = 9001
assert config_eos.server.port == 9000
assert config_eos.server.eosdash_port == 9001
class TestServerStartStop:
@pytest.mark.asyncio

View File

@@ -7,11 +7,11 @@
| Name | Environment Variable | Type | Read-Only | Default | Description |
| ---- | -------------------- | ---- | --------- | ------- | ----------- |
| eosdash_host | `EOS_SERVER__EOSDASH_HOST` | `Optional[str]` | `rw` | `None` | EOSdash server IP address. Defaults to EOS server IP address. |
| eosdash_port | `EOS_SERVER__EOSDASH_PORT` | `Optional[int]` | `rw` | `None` | EOSdash server IP port number. Defaults to EOS server IP port number + 1. |
| eosdash_host | `EOS_SERVER__EOSDASH_HOST` | `str` | `rw` | `127.0.0.1` | EOSdash server IP address. Defaults to EOS server IP address. |
| eosdash_port | `EOS_SERVER__EOSDASH_PORT` | `int` | `rw` | `8504` | EOSdash server IP port number. Defaults to 8504. |
| eosdash_supervise_interval_sec | `EOS_SERVER__EOSDASH_SUPERVISE_INTERVAL_SEC` | `int` | `rw` | `10` | Supervision interval for EOS server to supervise EOSdash [seconds]. |
| host | `EOS_SERVER__HOST` | `Optional[str]` | `rw` | `127.0.0.1` | EOS server IP address. Defaults to 127.0.0.1. |
| port | `EOS_SERVER__PORT` | `Optional[int]` | `rw` | `8503` | EOS server IP port number. Defaults to 8503. |
| host | `EOS_SERVER__HOST` | `str` | `rw` | `127.0.0.1` | EOS server IP address. Defaults to 127.0.0.1. |
| port | `EOS_SERVER__PORT` | `int` | `rw` | `8503` | EOS server IP port number. Defaults to 8503. |
| reload | `EOS_SERVER__RELOAD` | `Optional[bool]` | `rw` | `False` | Enable server auto-reload for debugging or development. Default is False. Monitors the package directory for changes and reloads the server. |
| run_as_user | `EOS_SERVER__RUN_AS_USER` | `Optional[str]` | `rw` | `None` | The name of the target user to switch to. If ``None`` (default), the current effective user is used and no privilege change is attempted. |
| startup_eosdash | `EOS_SERVER__STARTUP_EOSDASH` | `Optional[bool]` | `rw` | `True` | EOS server to start EOSdash server. Defaults to True. |

View File

@@ -77,7 +77,6 @@
"server": {
"host": "0.0.0.0",
"verbose": true,
"eosdash_host": "0.0.0.0",
"eosdash_port": 8504
"eosdash_host": "0.0.0.0"
}
}

View File

@@ -14,7 +14,6 @@
},
"server": {
"host": "0.0.0.0",
"eosdash_host": "0.0.0.0",
"eosdash_port": 8504
"eosdash_host": "0.0.0.0"
}
}

View File

@@ -121,9 +121,5 @@
},
"weather": {
"provider": "BrightSky"
},
"server": {
"eosdash_host": "127.0.0.1",
"eosdash_port": 8504
}
}

24
uv.lock generated
View File

@@ -90,7 +90,7 @@ requires-dist = [
{ name = "docutils", marker = "extra == 'dev'", specifier = "==0.21.2" },
{ name = "fastapi", extras = ["standard-no-fastapi-cloud-cli"], specifier = "==0.139.2" },
{ name = "fastapi-cli", specifier = "==0.0.32" },
{ name = "gitpython", marker = "extra == 'dev'", specifier = "==3.1.52" },
{ name = "gitpython", marker = "extra == 'dev'", specifier = "==3.1.53" },
{ name = "linkify-it-py", specifier = "==2.1.0" },
{ name = "lmdb", specifier = "==2.3.0" },
{ name = "loguru", specifier = "==0.7.3" },
@@ -105,8 +105,8 @@ requires-dist = [
{ name = "pandas", specifier = "==3.0.3" },
{ name = "pandas-stubs", marker = "extra == 'dev'", specifier = "==3.0.3.260530" },
{ name = "pendulum", specifier = "==3.2.0" },
{ name = "platformdirs", specifier = "==4.10.1" },
{ name = "pre-commit", marker = "extra == 'dev'", specifier = "==4.6.0" },
{ name = "platformdirs", specifier = "==4.11.0" },
{ name = "pre-commit", marker = "extra == 'dev'", specifier = "==4.6.1" },
{ name = "psutil", specifier = "==7.2.2" },
{ name = "pvlib", specifier = "==0.15.2" },
{ name = "pydantic", specifier = "==2.13.4" },
@@ -1102,14 +1102,14 @@ wheels = [
[[package]]
name = "gitpython"
version = "3.1.52"
version = "3.1.53"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "gitdb" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e5/fd/df0bafa4eb5ea2f51e1adee9f7a94c8e62c5d180e65117045dfca3439c8a/gitpython-3.1.52.tar.gz", hash = "sha256:de0a8ad86274c6e75ae8b37dd055ba68f19818c813108642263227b20775b48e", size = 223726, upload-time = "2026-07-16T03:15:59.599Z" }
sdist = { url = "https://files.pythonhosted.org/packages/17/24/0e0c12cb6f7cb864779a9d2fefee9ca91838f6db402c8780c9d28a8d7ebe/gitpython-3.1.53.tar.gz", hash = "sha256:06ae8d9623b0ed0d67b8adeac5c7008d0a5a404b087a9e0d0c7163bdd3a6b497", size = 224597, upload-time = "2026-07-20T13:41:52.839Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8d/90/04dff7c1e176bb1c3011ef1647393d368790da710d8dde1cdcfad301f45a/gitpython-3.1.52-py3-none-any.whl", hash = "sha256:79a36ee1f83523214a3f72d56cf1c4e490d577dc61af77e43dfe5862bd9da01a", size = 215366, upload-time = "2026-07-16T03:15:58.239Z" },
{ url = "https://files.pythonhosted.org/packages/cf/a6/bff12b3238885eeef7d28ef908b24e0cba91c476c31cb876a00a0986ce2c/gitpython-3.1.53-py3-none-any.whl", hash = "sha256:187885556b64ab357bd4ea84e2c4cce2861a613a7f4268b3f7f7ba05f2ce4ab0", size = 216237, upload-time = "2026-07-20T13:41:51.473Z" },
]
[[package]]
@@ -2345,11 +2345,11 @@ wheels = [
[[package]]
name = "platformdirs"
version = "4.10.1"
version = "4.11.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/52/cd/4f25b2f95b23f5d2c9c1fe43e49841bff5800562149b2666afc09309aa8f/platformdirs-4.10.1.tar.gz", hash = "sha256:ceab4084426fe6319ce18e86deada8ab1b7487c7aee7040c55e277c9ae793695", size = 31678, upload-time = "2026-07-18T03:53:43.808Z" }
sdist = { url = "https://files.pythonhosted.org/packages/78/9b/560e4be8e26f6fd133a03630a8df0c663b9e8d61b4ade152b72005aec83b/platformdirs-4.11.0.tar.gz", hash = "sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0", size = 31953, upload-time = "2026-07-21T13:09:36.565Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/73/6fd0bb9ce84138c3857f12e9de63bc901852975a092d545f18087a204aa2/platformdirs-4.10.1-py3-none-any.whl", hash = "sha256:0e4eff26be2d75293977f7cddc153fd9b8eaa7fb0c7b64ffe4076cb443117443", size = 22906, upload-time = "2026-07-18T03:53:42.576Z" },
{ url = "https://files.pythonhosted.org/packages/7d/68/d8d58938dfb1370b266a1a729e6d77a985be23689a0496498ee17b2cbf90/platformdirs-4.11.0-py3-none-any.whl", hash = "sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74", size = 23247, upload-time = "2026-07-21T13:09:35.422Z" },
]
[[package]]
@@ -2363,7 +2363,7 @@ wheels = [
[[package]]
name = "pre-commit"
version = "4.6.0"
version = "4.6.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cfgv" },
@@ -2372,9 +2372,9 @@ dependencies = [
{ name = "pyyaml" },
{ name = "virtualenv" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" }
sdist = { url = "https://files.pythonhosted.org/packages/25/3a/ddb78f32a0814e66b18a099377a106a2dcdce92d86a034d69d65df9b256e/pre_commit-4.6.1.tar.gz", hash = "sha256:03e809865c7d178b9979d06c761fcbfe6808fdaded8581a745bb110e52050421", size = 198646, upload-time = "2026-07-21T20:56:58.225Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" },
{ url = "https://files.pythonhosted.org/packages/fb/49/bc925106abcdac498074f2cbe6137e94e09f418dd2b7775df5b577dc0313/pre_commit-4.6.1-py2.py3-none-any.whl", hash = "sha256:0e3b2942510d1fb34eec167a3ec57331bf8442122f1153a9fb8b58f5c49b2717", size = 226186, upload-time = "2026-07-21T20:56:57.064Z" },
]
[[package]]