From 0306e7e4ed28829344ac5f9583a09588900fc10f Mon Sep 17 00:00:00 2001 From: Bobby Noelte Date: Thu, 16 Jul 2026 17:15:10 +0200 Subject: [PATCH] fix: prediction import with pydantic-typed bodies (#1152) Respect pydantic model in serializing json data for import. Signed-off-by: Bobby Noelte --- .env | 2 +- config.yaml | 2 +- docs/_generated/openapi.md | 2 +- openapi.json | 2 +- src/akkudoktoreos/prediction/feedintariff.py | 8 +-- src/akkudoktoreos/server/eos.py | 9 +++- tests/test_system.py | 51 ++++++++++++++++++++ tests/testdata/eosserver_config_1.json | 3 ++ uv.lock | 8 +-- 9 files changed, 73 insertions(+), 14 deletions(-) diff --git a/.env b/.env index 7264f758..4d8580ef 100644 --- a/.env +++ b/.env @@ -11,7 +11,7 @@ DOCKER_COMPOSE_DATA_DIR=${HOME}/.local/share/net.akkudoktor.eos # ----------------------------------------------------------------------------- # Image / build # ----------------------------------------------------------------------------- -VERSION=0.3.0.dev2607150960221650 +VERSION=0.3.0.dev2607161418098328 PYTHON_VERSION=3.13.9 # ----------------------------------------------------------------------------- diff --git a/config.yaml b/config.yaml index 605c554c..4ef5e3ac 100644 --- a/config.yaml +++ b/config.yaml @@ -6,7 +6,7 @@ # the root directory (no add-on folder as usual). name: "Akkudoktor-EOS" -version: "0.3.0.dev2607150960221650" +version: "0.3.0.dev2607161418098328" slug: "eos" description: "Akkudoktor-EOS add-on" url: "https://github.com/Akkudoktor-EOS/EOS" diff --git a/docs/_generated/openapi.md b/docs/_generated/openapi.md index f0af6c6f..9343ead3 100644 --- a/docs/_generated/openapi.md +++ b/docs/_generated/openapi.md @@ -1,6 +1,6 @@ # Akkudoktor-EOS -**Version**: `v0.3.0.dev2607150960221650` +**Version**: `v0.3.0.dev2607161418098328` **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. diff --git a/openapi.json b/openapi.json index 7e2f35ec..6b6426c6 100644 --- a/openapi.json +++ b/openapi.json @@ -8,7 +8,7 @@ "name": "Apache 2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0.html" }, - "version": "v0.3.0.dev2607150960221650" + "version": "v0.3.0.dev2607161418098328" }, "paths": { "/v1/admin/cache/clear": { diff --git a/src/akkudoktoreos/prediction/feedintariff.py b/src/akkudoktoreos/prediction/feedintariff.py index 4c5ac8d4..559711a2 100644 --- a/src/akkudoktoreos/prediction/feedintariff.py +++ b/src/akkudoktoreos/prediction/feedintariff.py @@ -9,7 +9,7 @@ from akkudoktoreos.prediction.feedintarifffixed import FeedInTariffFixedCommonSe from akkudoktoreos.prediction.feedintariffimport import FeedInTariffImportCommonSettings -def elecprice_provider_ids() -> list[str]: +def feedintariff_provider_ids() -> list[str]: """Valid feedintariff provider ids.""" try: prediction_eos = get_prediction() @@ -67,14 +67,14 @@ class FeedInTariffCommonSettings(SettingsBaseModel): @property def providers(self) -> list[str]: """Available feed in tariff provider ids.""" - return elecprice_provider_ids() + return feedintariff_provider_ids() # Validators @field_validator("provider", mode="after") @classmethod def validate_provider(cls, value: Optional[str]) -> Optional[str]: - if value is None or value in elecprice_provider_ids(): + if value is None or value in feedintariff_provider_ids(): return value raise ValueError( - f"Provider '{value}' is not a valid feed in tariff provider: {elecprice_provider_ids()}." + f"Provider '{value}' is not a valid feed in tariff provider: {feedintariff_provider_ids()}." ) diff --git a/src/akkudoktoreos/server/eos.py b/src/akkudoktoreos/server/eos.py index 06620b5f..f377cf7a 100755 --- a/src/akkudoktoreos/server/eos.py +++ b/src/akkudoktoreos/server/eos.py @@ -40,6 +40,7 @@ from akkudoktoreos.core.ems import ems_manage_energy from akkudoktoreos.core.emsettings import EnergyManagementMode from akkudoktoreos.core.logging import logging_track_config, read_file_log from akkudoktoreos.core.pydantic import ( + BaseModel, PydanticBaseModel, PydanticDateTimeData, PydanticDateTimeDataFrame, @@ -1036,7 +1037,7 @@ async def fastapi_prediction_list_get( @app.put("/v1/prediction/import/{provider_id}", tags=["prediction"]) -def fastapi_prediction_import_provider( +async def fastapi_prediction_import_provider( provider_id: str = FastapiPath(..., description="Provider ID."), data: Optional[Union[PydanticDateTimeDataFrame, PydanticDateTimeData, dict]] = None, force_enable: Optional[bool] = None, @@ -1056,7 +1057,11 @@ def fastapi_prediction_import_provider( if not provider.enabled() and not force_enable: raise HTTPException(status_code=404, detail=f"Provider '{provider_id}' not enabled.") try: - provider.import_from_json(json_str=json.dumps(data)) + if isinstance(data, BaseModel): + json_str = data.model_dump_json() + else: + json_str = json.dumps(data) + await provider.import_from_json(json_str=json_str) provider.update_datetime = to_datetime(in_timezone=get_config().general.timezone) except Exception as e: raise HTTPException( diff --git a/tests/test_system.py b/tests/test_system.py index eb3659bd..36809ac9 100644 --- a/tests/test_system.py +++ b/tests/test_system.py @@ -170,6 +170,57 @@ class TestSystem: else: pass + def test_prediction_import(self, server_setup_for_class, is_system_test): + """Test eprediction import.""" + server = server_setup_for_class["server"] + eos_dir = server_setup_for_class["eos_dir"] + + # Reset config + with FILE_TESTDATA_EOSSERVER_CONFIG_1.open("r", encoding="utf-8", newline=None) as fd: + config = json.load(fd) + config["elecprice"]["provider"] = "ElecPriceImport" + result = requests.put(f"{server}/v1/config", json=config) + assert result.status_code == HTTPStatus.OK + + # Assure prediction is enabled + result = requests.get(f"{server}/v1/prediction/providers?enabled=true") + assert result.status_code == HTTPStatus.OK + providers = result.json() + assert "ElecPriceImport" in providers + + # Elex price payload + payload = { + "start_datetime": "2026-05-27 00:00:00", + "interval": "1 hour", + "elecprice_marketprice_wh": [0.000234, 0.000228, 0.000231], + } + + result = requests.put( + f"{server}/v1/prediction/import/ElecPriceImport", + params={"force_enable": True}, + json=payload, + ) + assert result.status_code == HTTPStatus.OK + + # Import should work with any provider + config["feedintariff"]["provider"] = "FeedInTariffImport" + result = requests.put(f"{server}/v1/config", json=config) + assert result.status_code == HTTPStatus.OK + + # Feed In Tariff payload + payload = { + "start_datetime": "2026-05-27 00:00:00", + "interval": "1 hour", + "feed_in_tariff_wh": [0.000071, 0.000072, 0.000073], + } + + result = requests.put( + f"{server}/v1/prediction/import/FeedInTariffImport", + params={"force_enable": True}, + json=payload, + ) + assert result.status_code == HTTPStatus.OK + def test_measurement(self, server_setup_for_class, is_system_test): """Test measurement endpoints comprehensively.""" server = server_setup_for_class["server"] diff --git a/tests/testdata/eosserver_config_1.json b/tests/testdata/eosserver_config_1.json index 6c4811d0..044fbbbc 100644 --- a/tests/testdata/eosserver_config_1.json +++ b/tests/testdata/eosserver_config_1.json @@ -3,6 +3,9 @@ "charges_kwh": 0.21, "provider": "ElecPriceImport" }, + "feedintariff": { + "provider": "FeedInTariffFixed" + }, "general": { "latitude": 52.5, "longitude": 13.4 diff --git a/uv.lock b/uv.lock index 6da17c20..07ecbb08 100644 --- a/uv.lock +++ b/uv.lock @@ -116,7 +116,7 @@ requires-dist = [ { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = "==1.4.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = "==7.1.0" }, { name = "pytest-xprocess", marker = "extra == 'dev'", specifier = "==1.0.2" }, - { name = "python-fasthtml", specifier = "==0.14.5" }, + { name = "python-fasthtml", specifier = "==0.14.6" }, { name = "requests", specifier = "==2.34.2" }, { name = "rich-toolkit", specifier = "==0.20.3" }, { name = "scipy", specifier = "==1.17.1" }, @@ -2697,7 +2697,7 @@ wheels = [ [[package]] name = "python-fasthtml" -version = "0.14.5" +version = "0.14.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beautifulsoup4" }, @@ -2711,9 +2711,9 @@ dependencies = [ { name = "starlette" }, { name = "uvicorn", extra = ["standard"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/9b/e442133c5d950d9ccc0a879357a3c108e1dcfd80a19bd601459a615c1a7e/python_fasthtml-0.14.5.tar.gz", hash = "sha256:d0181c36415f9bbe242e1d05de1fb5279a14aa9c605c244e76ae95a32650eda4", size = 76817, upload-time = "2026-07-09T05:46:10.028Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/db/1efed9c205c9f65a1e9715f91aff1510fdc42e41f8c032718274969b35a7/python_fasthtml-0.14.6.tar.gz", hash = "sha256:11b535e752b4f79e8da7eb6d55928d2ced0ed7a085619ac89a6e943a76915773", size = 77102, upload-time = "2026-07-11T04:03:42.889Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/63/5685ed7eff6b581bcfd0db4e239c8d07d0e1e59c016b409a1c01c10aab64/python_fasthtml-0.14.5-py3-none-any.whl", hash = "sha256:e5ce3cc43af056ac5d48b1cdcb9346111fdb8a21fa0fcf788d2490c8ea12dc9f", size = 80441, upload-time = "2026-07-09T05:46:08.427Z" }, + { url = "https://files.pythonhosted.org/packages/69/d6/70783f684b6c367466c4288005e0f95cdf9ffdd1d8f397fad921f9567d10/python_fasthtml-0.14.6-py3-none-any.whl", hash = "sha256:38a93cfb87911ae34aaa088e3b53784a5ed72c575bc9656df7599d82428ab41c", size = 80779, upload-time = "2026-07-11T04:03:41.222Z" }, ] [[package]]