199 Commits

Author SHA1 Message Date
dependabot[bot]
e5bb83b915 build(deps-dev): bump pandas-stubs from 2.3.2.250827 to 2.3.2.250926
Bumps [pandas-stubs](https://github.com/pandas-dev/pandas-stubs) from 2.3.2.250827 to 2.3.2.250926.
- [Changelog](https://github.com/pandas-dev/pandas-stubs/blob/main/docs/release_procedure.md)
- [Commits](https://github.com/pandas-dev/pandas-stubs/compare/v2.3.2.250827...v2.3.2.250926)

---
updated-dependencies:
- dependency-name: pandas-stubs
  dependency-version: 2.3.2.250926
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-27 21:47:09 +00:00
dependabot[bot]
cdb8bb26a9 build(deps): bump pydantic-settings from 2.10.1 to 2.11.0 (#698)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Bumps [pydantic-settings](https://github.com/pydantic/pydantic-settings) from 2.10.1 to 2.11.0.
- [Release notes](https://github.com/pydantic/pydantic-settings/releases)
- [Commits](https://github.com/pydantic/pydantic-settings/compare/2.10.1...v2.11.0)

---
updated-dependencies:
- dependency-name: pydantic-settings
  dependency-version: 2.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-24 21:52:25 +02:00
dependabot[bot]
9a07b010ca build(deps): bump pvlib from 0.13.0 to 0.13.1 (#699)
Bumps [pvlib](https://github.com/pvlib/pvlib-python) from 0.13.0 to 0.13.1.
- [Release notes](https://github.com/pvlib/pvlib-python/releases)
- [Commits](https://github.com/pvlib/pvlib-python/compare/v0.13.0...v0.13.1)

---
updated-dependencies:
- dependency-name: pvlib
  dependency-version: 0.13.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-24 21:52:01 +02:00
dependabot[bot]
a70a1f5ca0 build(deps): bump python-fasthtml from 0.12.28 to 0.12.29 (#697)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Bumps [python-fasthtml](https://github.com/AnswerDotAI/fasthtml) from 0.12.28 to 0.12.29.
- [Release notes](https://github.com/AnswerDotAI/fasthtml/releases)
- [Changelog](https://github.com/AnswerDotAI/fasthtml/blob/main/CHANGELOG.md)
- [Commits](https://github.com/AnswerDotAI/fasthtml/compare/0.12.28...0.12.29)

---
updated-dependencies:
- dependency-name: python-fasthtml
  dependency-version: 0.12.29
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-23 13:16:58 +02:00
dependabot[bot]
86a8985235 build(deps): bump psutil from 7.0.0 to 7.1.0 (#694)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Bumps [psutil](https://github.com/giampaolo/psutil) from 7.0.0 to 7.1.0.
- [Changelog](https://github.com/giampaolo/psutil/blob/master/HISTORY.rst)
- [Commits](https://github.com/giampaolo/psutil/compare/release-7.0.0...release-7.1.0)

---
updated-dependencies:
- dependency-name: psutil
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-21 20:45:11 +02:00
dependabot[bot]
d3ba5bb464 build(deps): bump uvicorn from 0.35.0 to 0.36.0 (#689)
Bumps [uvicorn](https://github.com/Kludex/uvicorn) from 0.35.0 to 0.36.0.
- [Release notes](https://github.com/Kludex/uvicorn/releases)
- [Changelog](https://github.com/Kludex/uvicorn/blob/main/docs/release-notes.md)
- [Commits](https://github.com/Kludex/uvicorn/compare/0.35.0...0.36.0)

---
updated-dependencies:
- dependency-name: uvicorn
  dependency-version: 0.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-21 20:36:30 +02:00
dependabot[bot]
5df8a19f13 build(deps-dev): bump mypy from 1.18.1 to 1.18.2 (#690)
Bumps [mypy](https://github.com/python/mypy) from 1.18.1 to 1.18.2.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.18.1...v1.18.2)

---
updated-dependencies:
- dependency-name: mypy
  dependency-version: 1.18.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-21 20:36:14 +02:00
dependabot[bot]
482227c0b1 build(deps): bump python-fasthtml from 0.12.27 to 0.12.28 (#691)
Bumps [python-fasthtml](https://github.com/AnswerDotAI/fasthtml) from 0.12.27 to 0.12.28.
- [Release notes](https://github.com/AnswerDotAI/fasthtml/releases)
- [Changelog](https://github.com/AnswerDotAI/fasthtml/blob/main/CHANGELOG.md)
- [Commits](https://github.com/AnswerDotAI/fasthtml/compare/0.12.27...0.12.28)

---
updated-dependencies:
- dependency-name: python-fasthtml
  dependency-version: 0.12.28
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-21 20:35:55 +02:00
dependabot[bot]
0a6a6d5ad8 build(deps): bump monsterui from 1.0.28 to 1.0.29 (#688)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Bumps [monsterui](https://github.com/AnswerDotAI/MonsterUI) from 1.0.28 to 1.0.29.
- [Release notes](https://github.com/AnswerDotAI/MonsterUI/releases)
- [Changelog](https://github.com/AnswerDotAI/MonsterUI/blob/main/CHANGELOG.bak)
- [Commits](https://github.com/AnswerDotAI/MonsterUI/compare/1.0.28...1.0.29)

---
updated-dependencies:
- dependency-name: monsterui
  dependency-version: 1.0.29
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-16 10:47:45 +02:00
dependabot[bot]
64db2a7c0e build(deps): bump python-fasthtml from 0.12.25 to 0.12.27
Some checks failed
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Bumps [python-fasthtml](https://github.com/AnswerDotAI/fasthtml) from 0.12.25 to 0.12.27.
- [Release notes](https://github.com/AnswerDotAI/fasthtml/releases)
- [Changelog](https://github.com/AnswerDotAI/fasthtml/blob/main/CHANGELOG.md)
- [Commits](https://github.com/AnswerDotAI/fasthtml/compare/0.12.25...0.12.27)

---
updated-dependencies:
- dependency-name: python-fasthtml
  dependency-version: 0.12.27
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-14 22:26:17 +00:00
dependabot[bot]
ff713ca854 build(deps-dev): bump types-requests
Bumps [types-requests](https://github.com/typeshed-internal/stub_uploader) from 2.32.4.20250809 to 2.32.4.20250913.
- [Commits](https://github.com/typeshed-internal/stub_uploader/commits)

---
updated-dependencies:
- dependency-name: types-requests
  dependency-version: 2.32.4.20250913
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-14 22:14:53 +00:00
dependabot[bot]
2ec54e3a99 build(deps): bump pydantic from 2.11.7 to 2.11.9
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.11.7 to 2.11.9.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/v2.11.9/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.11.7...v2.11.9)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-version: 2.11.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-14 22:13:10 +00:00
dependabot[bot]
e3f271d1f3 build(deps): bump timezonefinder from 7.0.1 to 7.0.2
Bumps [timezonefinder](https://github.com/jannikmi/timezonefinder) from 7.0.1 to 7.0.2.
- [Release notes](https://github.com/jannikmi/timezonefinder/releases)
- [Changelog](https://github.com/jannikmi/timezonefinder/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jannikmi/timezonefinder/compare/7.0.1...7.0.2)

---
updated-dependencies:
- dependency-name: timezonefinder
  dependency-version: 7.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-14 22:12:48 +00:00
dependabot[bot]
3c0d0b32df build(deps): bump cachebox from 5.0.1 to 5.0.2
Bumps [cachebox](https://github.com/awolverp/cachebox) from 5.0.1 to 5.0.2.
- [Release notes](https://github.com/awolverp/cachebox/releases)
- [Changelog](https://github.com/awolverp/cachebox/blob/main/CHANGELOG.md)
- [Commits](https://github.com/awolverp/cachebox/compare/v5.0.1...v5.0.2)

---
updated-dependencies:
- dependency-name: cachebox
  dependency-version: 5.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-14 22:05:30 +00:00
dependabot[bot]
e8086defd2 build(deps-dev): bump mypy from 1.17.1 to 1.18.1
Bumps [mypy](https://github.com/python/mypy) from 1.17.1 to 1.18.1.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.17.1...v1.18.1)

---
updated-dependencies:
- dependency-name: mypy
  dependency-version: 1.18.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-14 22:05:09 +00:00
dependabot[bot]
559699cdad build(deps): bump monsterui from 1.0.26 to 1.0.28
Bumps [monsterui](https://github.com/AnswerDotAI/MonsterUI) from 1.0.26 to 1.0.28.
- [Release notes](https://github.com/AnswerDotAI/MonsterUI/releases)
- [Changelog](https://github.com/AnswerDotAI/MonsterUI/blob/main/CHANGELOG.bak)
- [Commits](https://github.com/AnswerDotAI/MonsterUI/compare/1.0.26...1.0.28)

---
updated-dependencies:
- dependency-name: monsterui
  dependency-version: 1.0.28
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-14 22:04:33 +00:00
boerni
a4cce85ebd add configurable VAT rate for electricity price calculations (#680)
* add configurable VAT rate for electricity price calculations

* add VAT rate configuration for electricity price calculations in docs

* added vat_rate

* fix: Format VAT rate field definition according to ruff formatting standards

---------

Co-authored-by: Börni <kontakt@bernhardhientz.com>
2025-09-14 23:53:41 +02:00
dependabot[bot]
d362aa7298 build(deps): bump scikit-learn from 1.7.1 to 1.7.2 (#679)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Bumps [scikit-learn](https://github.com/scikit-learn/scikit-learn) from 1.7.1 to 1.7.2.
- [Release notes](https://github.com/scikit-learn/scikit-learn/releases)
- [Commits](https://github.com/scikit-learn/scikit-learn/compare/1.7.1...1.7.2)

---
updated-dependencies:
- dependency-name: scikit-learn
  dependency-version: 1.7.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-11 09:07:19 +02:00
dependabot[bot]
1f610ce21c build(deps-dev): bump pytest-cov from 6.3.0 to 7.0.0 (#678)
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 6.3.0 to 7.0.0.
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v6.3.0...v7.0.0)

---
updated-dependencies:
- dependency-name: pytest-cov
  dependency-version: 7.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-11 08:56:59 +02:00
dependabot[bot]
c6884fb54c build(deps): bump numpy from 2.3.2 to 2.3.3 (#677)
Bumps [numpy](https://github.com/numpy/numpy) from 2.3.2 to 2.3.3.
- [Release notes](https://github.com/numpy/numpy/releases)
- [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst)
- [Commits](https://github.com/numpy/numpy/compare/v2.3.2...v2.3.3)

---
updated-dependencies:
- dependency-name: numpy
  dependency-version: 2.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-11 08:56:47 +02:00
7tobias
340ca49957 Fix horizon validation for non-integer angle divisions (#665)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Fixes #647 - Change azimuthFrom/azimuthTo to float type to handle horizon counts that don't divide evenly into 360°

Co-authored-by: Tobias Welz <tobias.wizneteu@gmail.com>
2025-09-09 10:50:14 +02:00
dependabot[bot]
09ad75d47d build(deps-dev): bump pytest from 8.4.1 to 8.4.2 (#671)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.4.1 to 8.4.2.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.4.1...8.4.2)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 8.4.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-07 16:29:42 +02:00
dependabot[bot]
5d1962aeab build(deps-dev): bump pytest-cov from 6.2.1 to 6.3.0 (#672)
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 6.2.1 to 6.3.0.
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v6.2.1...v6.3.0)

---
updated-dependencies:
- dependency-name: pytest-cov
  dependency-version: 6.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-07 16:18:30 +02:00
dependabot[bot]
b8dc4363ed build(deps): bump monsterui from 1.0.25 to 1.0.26 (#670)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Bumps [monsterui](https://github.com/AnswerDotAI/MonsterUI) from 1.0.25 to 1.0.26.
- [Release notes](https://github.com/AnswerDotAI/MonsterUI/releases)
- [Changelog](https://github.com/AnswerDotAI/MonsterUI/blob/main/CHANGELOG.bak)
- [Commits](https://github.com/AnswerDotAI/MonsterUI/compare/1.0.25...1.0.26)

---
updated-dependencies:
- dependency-name: monsterui
  dependency-version: 1.0.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-31 21:37:34 +02:00
dependabot[bot]
22809c1e9b build(deps): bump matplotlib from 3.10.5 to 3.10.6 (#669)
Bumps [matplotlib](https://github.com/matplotlib/matplotlib) from 3.10.5 to 3.10.6.
- [Release notes](https://github.com/matplotlib/matplotlib/releases)
- [Commits](https://github.com/matplotlib/matplotlib/compare/v3.10.5...v3.10.6)

---
updated-dependencies:
- dependency-name: matplotlib
  dependency-version: 3.10.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-31 21:37:17 +02:00
dependabot[bot]
e007a60c60 build(deps-dev): bump pandas-stubs from 2.3.0.250703 to 2.3.2.250827 (#667)
Bumps [pandas-stubs](https://github.com/pandas-dev/pandas-stubs) from 2.3.0.250703 to 2.3.2.250827.
- [Changelog](https://github.com/pandas-dev/pandas-stubs/blob/main/docs/release_procedure.md)
- [Commits](https://github.com/pandas-dev/pandas-stubs/compare/v2.3.0.250703...v2.3.2.250827)

---
updated-dependencies:
- dependency-name: pandas-stubs
  dependency-version: 2.3.2.250827
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-31 21:29:48 +02:00
dependabot[bot]
efd115e8d4 build(deps): bump bokeh from 3.7.3 to 3.8.0 (#668)
Bumps [bokeh](https://github.com/bokeh/bokeh) from 3.7.3 to 3.8.0.
- [Changelog](https://github.com/bokeh/bokeh/blob/branch-3.9/docs/CHANGELOG)
- [Commits](https://github.com/bokeh/bokeh/compare/3.7.3...3.8.0)

---
updated-dependencies:
- dependency-name: bokeh
  dependency-version: 3.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-31 21:29:20 +02:00
7tobias
5881c1459a docs: Add Docker volume mount configuration instructions (#662)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
- Add documentation explaining the config file requirement when mounting volumes
- Include example EOS.config.json with required 0.0.0.0 host bindings
- Provide commented-out volume mount examples for easy user reference

This addresses issue #661 where users experience connection issues when
mounting local directories without proper configuration.

Co-authored-by: Tobias Welz <tobias.wizneteu@gmail.com>
2025-08-26 23:32:37 +02:00
dependabot[bot]
45a5ec9b66 build(deps): bump platformdirs from 4.3.8 to 4.4.0 (#664)
Bumps [platformdirs](https://github.com/tox-dev/platformdirs) from 4.3.8 to 4.4.0.
- [Release notes](https://github.com/tox-dev/platformdirs/releases)
- [Changelog](https://github.com/tox-dev/platformdirs/blob/main/CHANGES.rst)
- [Commits](https://github.com/tox-dev/platformdirs/compare/4.3.8...4.4.0)

---
updated-dependencies:
- dependency-name: platformdirs
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-26 21:38:09 +02:00
dependabot[bot]
4c13d561a2 build(deps): bump pandas from 2.3.1 to 2.3.2 (#663)
Bumps [pandas](https://github.com/pandas-dev/pandas) from 2.3.1 to 2.3.2.
- [Release notes](https://github.com/pandas-dev/pandas/releases)
- [Commits](https://github.com/pandas-dev/pandas/compare/v2.3.1...v2.3.2)

---
updated-dependencies:
- dependency-name: pandas
  dependency-version: 2.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-26 21:28:57 +02:00
dependabot[bot]
b34c528609 build(deps): bump python-fasthtml from 0.12.24 to 0.12.25 (#660)
Some checks failed
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Bumps [python-fasthtml](https://github.com/AnswerDotAI/fasthtml) from 0.12.24 to 0.12.25.
- [Release notes](https://github.com/AnswerDotAI/fasthtml/releases)
- [Changelog](https://github.com/AnswerDotAI/fasthtml/blob/main/CHANGELOG.md)
- [Commits](https://github.com/AnswerDotAI/fasthtml/compare/0.12.24...0.12.25)

---
updated-dependencies:
- dependency-name: python-fasthtml
  dependency-version: 0.12.25
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-22 17:10:58 +02:00
dependabot[bot]
7d18c3940e build(deps): bump monsterui from 1.0.24 to 1.0.25 (#659)
Bumps [monsterui](https://github.com/AnswerDotAI/MonsterUI) from 1.0.24 to 1.0.25.
- [Release notes](https://github.com/AnswerDotAI/MonsterUI/releases)
- [Changelog](https://github.com/AnswerDotAI/MonsterUI/blob/main/CHANGELOG.bak)
- [Commits](https://github.com/AnswerDotAI/MonsterUI/compare/1.0.24...1.0.25)

---
updated-dependencies:
- dependency-name: monsterui
  dependency-version: 1.0.25
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-22 17:02:34 +02:00
7tobias
decae10434 fix: handle float values in userhorizon configuration (#657)
Replace int() with round() when converting userhorizon values to properly
handle float values in the configuration. This prevents validation errors
when users provide horizon angles with decimal precision.

The Akkudoktor API expects integer values, so rounding to the nearest
integer maintains compatibility while accepting float inputs.

Fixes #647

Co-authored-by: Tobias Welz <tobias-wizneteu@gmail.com>
2025-08-22 16:41:40 +02:00
7tobias
cacb21529b fix(docker): make EOSDash accessible in Docker containers (#656)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Fixes the issue where EOSDash is not accessible when running EOS in a
Docker container. The problem was that EOSDash was binding to 127.0.0.1
(localhost) by default, making it inaccessible from outside the container.

The fix adds a minimal default configuration file during the Docker image
build that sets both the EOS server and EOSDash to bind to 0.0.0.0,
allowing external access while maintaining security through Docker's
network isolation.

- Add default EOS.config.json in Dockerfile with server bindings set to 0.0.0.0
- No changes required to docker-compose.yaml
- Ensures EOSDash works out of the box for Docker users

Fixes: #629
Closes: https://github.com/Akkudoktor-EOS/EOS/issues/629

Co-authored-by: Tobias Welz <tobias.welz@wiznet.eu>
2025-08-21 15:30:27 +02:00
dependabot[bot]
eed7c9ce58 build(deps-dev): bump pymarkdownlnt from 0.9.31 to 0.9.32 (#655)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Bumps [pymarkdownlnt](https://github.com/jackdewinter/pymarkdown) from 0.9.31 to 0.9.32.
- [Release notes](https://github.com/jackdewinter/pymarkdown/releases)
- [Changelog](https://github.com/jackdewinter/pymarkdown/blob/main/changelog.md)
- [Commits](https://github.com/jackdewinter/pymarkdown/compare/v0.9.31...v0.9.32)

---
updated-dependencies:
- dependency-name: pymarkdownlnt
  dependency-version: 0.9.32
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-19 15:49:43 +02:00
dependabot[bot]
4a87c67909 build(deps): bump numpydantic from 1.6.10 to 1.6.11 (#654)
Bumps [numpydantic](https://github.com/p2p-ld/numpydantic) from 1.6.10 to 1.6.11.
- [Release notes](https://github.com/p2p-ld/numpydantic/releases)
- [Changelog](https://github.com/p2p-ld/numpydantic/blob/main/docs/changelog.md)
- [Commits](https://github.com/p2p-ld/numpydantic/compare/v1.6.10...v1.6.11)

---
updated-dependencies:
- dependency-name: numpydantic
  dependency-version: 1.6.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-19 15:49:09 +02:00
dependabot[bot]
8319fed71c build(deps): bump requests from 2.32.4 to 2.32.5 (#652)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
Bumps [requests](https://github.com/psf/requests) from 2.32.4 to 2.32.5.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.32.4...v2.32.5)

---
updated-dependencies:
- dependency-name: requests
  dependency-version: 2.32.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-19 01:03:03 +02:00
dependabot[bot]
82c36b1ecd build(deps): bump monsterui from 1.0.23 to 1.0.24 (#651)
Bumps [monsterui](https://github.com/AnswerDotAI/MonsterUI) from 1.0.23 to 1.0.24.
- [Release notes](https://github.com/AnswerDotAI/MonsterUI/releases)
- [Changelog](https://github.com/AnswerDotAI/MonsterUI/blob/main/CHANGELOG.bak)
- [Commits](https://github.com/AnswerDotAI/MonsterUI/compare/1.0.23...1.0.24)

---
updated-dependencies:
- dependency-name: monsterui
  dependency-version: 1.0.24
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-19 01:01:43 +02:00
dependabot[bot]
0378fa0f52 build(deps-dev): bump types-requests (#650)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Bumps [types-requests](https://github.com/typeshed-internal/stub_uploader) from 2.32.4.20250611 to 2.32.4.20250809.
- [Commits](https://github.com/typeshed-internal/stub_uploader/commits)

---
updated-dependencies:
- dependency-name: types-requests
  dependency-version: 2.32.4.20250809
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 18:35:04 +02:00
dependabot[bot]
f79ec947d6 build(deps): bump numpydantic from 1.6.9 to 1.6.10 (#644)
Bumps [numpydantic](https://github.com/p2p-ld/numpydantic) from 1.6.9 to 1.6.10.
- [Release notes](https://github.com/p2p-ld/numpydantic/releases)
- [Changelog](https://github.com/p2p-ld/numpydantic/blob/main/docs/changelog.md)
- [Commits](https://github.com/p2p-ld/numpydantic/compare/v1.6.9...v1.6.10)

---
updated-dependencies:
- dependency-name: numpydantic
  dependency-version: 1.6.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 18:22:08 +02:00
dependabot[bot]
a912c22752 build(deps): bump python-fasthtml from 0.12.23 to 0.12.24 (#645)
Bumps [python-fasthtml](https://github.com/AnswerDotAI/fasthtml) from 0.12.23 to 0.12.24.
- [Release notes](https://github.com/AnswerDotAI/fasthtml/releases)
- [Changelog](https://github.com/AnswerDotAI/fasthtml/blob/main/CHANGELOG.md)
- [Commits](https://github.com/AnswerDotAI/fasthtml/compare/0.12.23...0.12.24)

---
updated-dependencies:
- dependency-name: python-fasthtml
  dependency-version: 0.12.24
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 17:59:06 +02:00
dependabot[bot]
370975fc11 build(deps): bump mdit-py-plugins from 0.4.2 to 0.5.0 (#648)
Bumps [mdit-py-plugins](https://github.com/executablebooks/mdit-py-plugins) from 0.4.2 to 0.5.0.
- [Release notes](https://github.com/executablebooks/mdit-py-plugins/releases)
- [Changelog](https://github.com/executablebooks/mdit-py-plugins/blob/master/CHANGELOG.md)
- [Commits](https://github.com/executablebooks/mdit-py-plugins/compare/v0.4.2...v0.5.0)

---
updated-dependencies:
- dependency-name: mdit-py-plugins
  dependency-version: 0.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 17:58:44 +02:00
dependabot[bot]
ec8b226ef4 build(deps): bump monsterui from 1.0.21 to 1.0.23 (#643)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Bumps [monsterui](https://github.com/AnswerDotAI/MonsterUI) from 1.0.21 to 1.0.23.
- [Release notes](https://github.com/AnswerDotAI/MonsterUI/releases)
- [Changelog](https://github.com/AnswerDotAI/MonsterUI/blob/main/CHANGELOG.bak)
- [Commits](https://github.com/AnswerDotAI/MonsterUI/compare/1.0.21...1.0.23)

---
updated-dependencies:
- dependency-name: monsterui
  dependency-version: 1.0.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 12:02:34 +02:00
dependabot[bot]
33c4a736fe build(deps): bump python-fasthtml from 0.12.22 to 0.12.23 (#642)
Bumps [python-fasthtml](https://github.com/AnswerDotAI/fasthtml) from 0.12.22 to 0.12.23.
- [Release notes](https://github.com/AnswerDotAI/fasthtml/releases)
- [Changelog](https://github.com/AnswerDotAI/fasthtml/blob/main/CHANGELOG.md)
- [Commits](https://github.com/AnswerDotAI/fasthtml/compare/0.12.22...0.12.23)

---
updated-dependencies:
- dependency-name: python-fasthtml
  dependency-version: 0.12.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 11:06:00 +02:00
dependabot[bot]
02fac1229d build(deps): bump matplotlib from 3.10.3 to 3.10.5 (#641)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Bumps [matplotlib](https://github.com/matplotlib/matplotlib) from 3.10.3 to 3.10.5.
- [Release notes](https://github.com/matplotlib/matplotlib/releases)
- [Commits](https://github.com/matplotlib/matplotlib/compare/v3.10.3...v3.10.5)

---
updated-dependencies:
- dependency-name: matplotlib
  dependency-version: 3.10.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-01 00:51:43 +02:00
dependabot[bot]
4cc2ae0fb8 build(deps-dev): bump mypy from 1.17.0 to 1.17.1 (#640)
Bumps [mypy](https://github.com/python/mypy) from 1.17.0 to 1.17.1.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.17.0...v1.17.1)

---
updated-dependencies:
- dependency-name: mypy
  dependency-version: 1.17.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-01 00:51:27 +02:00
dependabot[bot]
e8af112859 build(deps-dev): bump gitpython from 3.1.44 to 3.1.45 (#639)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
---
updated-dependencies:
- dependency-name: gitpython
  dependency-version: 3.1.45
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-29 15:01:58 +02:00
dependabot[bot]
fde5758b92 build(deps): bump numpy from 2.3.1 to 2.3.2 (#636)
Bumps [numpy](https://github.com/numpy/numpy) from 2.3.1 to 2.3.2.
- [Release notes](https://github.com/numpy/numpy/releases)
- [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst)
- [Commits](https://github.com/numpy/numpy/compare/v2.3.1...v2.3.2)

---
updated-dependencies:
- dependency-name: numpy
  dependency-version: 2.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-29 15:01:40 +02:00
dependabot[bot]
d5b0a2bc78 build(deps): bump python-fasthtml from 0.12.21 to 0.12.22 (#637)
Bumps [python-fasthtml](https://github.com/AnswerDotAI/fasthtml) from 0.12.21 to 0.12.22.
- [Release notes](https://github.com/AnswerDotAI/fasthtml/releases)
- [Changelog](https://github.com/AnswerDotAI/fasthtml/blob/main/CHANGELOG.md)
- [Commits](https://github.com/AnswerDotAI/fasthtml/compare/0.12.21...0.12.22)

---
updated-dependencies:
- dependency-name: python-fasthtml
  dependency-version: 0.12.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-29 15:01:29 +02:00
dependabot[bot]
d09fa40507 build(deps): bump timezonefinder from 6.6.2 to 7.0.1 (#638)
Bumps [timezonefinder](https://github.com/jannikmi/timezonefinder) from 6.6.2 to 7.0.1.
- [Release notes](https://github.com/jannikmi/timezonefinder/releases)
- [Changelog](https://github.com/jannikmi/timezonefinder/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jannikmi/timezonefinder/compare/6.6.2...7.0.1)

---
updated-dependencies:
- dependency-name: timezonefinder
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-29 15:01:03 +02:00
dependabot[bot]
0c8a9efb65 build(deps): bump timezonefinder from 6.5.9 to 6.6.2 (#624)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Bumps [timezonefinder](https://github.com/jannikmi/timezonefinder) from 6.5.9 to 6.6.2.
- [Release notes](https://github.com/jannikmi/timezonefinder/releases)
- [Changelog](https://github.com/jannikmi/timezonefinder/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jannikmi/timezonefinder/compare/6.5.9...6.6.2)

---
updated-dependencies:
- dependency-name: timezonefinder
  dependency-version: 6.6.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-19 15:12:32 +02:00
dependabot[bot]
dbed3d3884 build(deps): bump scikit-learn from 1.7.0 to 1.7.1 (#625)
Bumps [scikit-learn](https://github.com/scikit-learn/scikit-learn) from 1.7.0 to 1.7.1.
- [Release notes](https://github.com/scikit-learn/scikit-learn/releases)
- [Commits](https://github.com/scikit-learn/scikit-learn/compare/1.7.0...1.7.1)

---
updated-dependencies:
- dependency-name: scikit-learn
  dependency-version: 1.7.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-19 14:54:37 +02:00
redmoon2711
8e69caba73 feat(VRM forecast): add load and pv forecast by VRM API (#611)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
Add support for fetching forecasts from the VRM API by Victron Energy. Retrieve forecasts for PV generation and total load of a Victron system from the internet. 

Tests for the new modules have been added, and the documentation has been updated accordingly.

Signed-off-by: redmoon2711 <redmoon2711@gmx.de>
2025-07-19 08:55:16 +02:00
dependabot[bot]
e5500b6857 build(deps-dev): bump pymarkdownlnt from 0.9.30 to 0.9.31 (#619)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
---
updated-dependencies:
- dependency-name: pymarkdownlnt
  dependency-version: 0.9.31
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-15 14:21:58 +02:00
dependabot[bot]
646d134f89 build(deps-dev): bump mypy from 1.16.1 to 1.17.0 (#620)
Bumps [mypy](https://github.com/python/mypy) from 1.16.1 to 1.17.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.16.1...v1.17.0)

---
updated-dependencies:
- dependency-name: mypy
  dependency-version: 1.17.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-15 14:21:40 +02:00
dependabot[bot]
08220eed02 build(deps): bump statsmodels from 0.14.4 to 0.14.5 (#615)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Bumps [statsmodels](https://github.com/statsmodels/statsmodels) from 0.14.4 to 0.14.5.
- [Release notes](https://github.com/statsmodels/statsmodels/releases)
- [Changelog](https://github.com/statsmodels/statsmodels/blob/main/CHANGES.md)
- [Commits](https://github.com/statsmodels/statsmodels/compare/v0.14.4...v0.14.5)

---
updated-dependencies:
- dependency-name: statsmodels
  dependency-version: 0.14.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-08 12:26:27 +02:00
dependabot[bot]
e4664abd21 build(deps): bump pandas from 2.3.0 to 2.3.1 (#617)
Bumps [pandas](https://github.com/pandas-dev/pandas) from 2.3.0 to 2.3.1.
- [Release notes](https://github.com/pandas-dev/pandas/releases)
- [Commits](https://github.com/pandas-dev/pandas/compare/v2.3.0...v2.3.1)

---
updated-dependencies:
- dependency-name: pandas
  dependency-version: 2.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-08 12:26:03 +02:00
dependabot[bot]
7f0340605f build(deps): bump python-fasthtml from 0.12.20 to 0.12.21 (#613)
Some checks failed
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Bumps [python-fasthtml](https://github.com/AnswerDotAI/fasthtml) from 0.12.20 to 0.12.21.
- [Release notes](https://github.com/AnswerDotAI/fasthtml/releases)
- [Changelog](https://github.com/AnswerDotAI/fasthtml/blob/main/CHANGELOG.md)
- [Commits](https://github.com/AnswerDotAI/fasthtml/compare/0.12.20...0.12.21)

---
updated-dependencies:
- dependency-name: python-fasthtml
  dependency-version: 0.12.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-06 14:16:57 +02:00
dependabot[bot]
8970b07e17 build(deps-dev): bump pandas-stubs from 2.2.3.250527 to 2.3.0.250703 (#612)
Bumps [pandas-stubs](https://github.com/pandas-dev/pandas-stubs) from 2.2.3.250527 to 2.3.0.250703.
- [Changelog](https://github.com/pandas-dev/pandas-stubs/blob/main/docs/release_procedure.md)
- [Commits](https://github.com/pandas-dev/pandas-stubs/commits)

---
updated-dependencies:
- dependency-name: pandas-stubs
  dependency-version: 2.3.0.250703
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-06 14:16:43 +02:00
dependabot[bot]
bfb13c68da build(deps): bump pydantic-settings from 2.10.0 to 2.10.1 (#609)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
---
updated-dependencies:
- dependency-name: pydantic-settings
  dependency-version: 2.10.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-06 13:09:51 +02:00
dependabot[bot]
1e5b04d006 build(deps): bump uvicorn from 0.34.3 to 0.35.0 (#610)
---
updated-dependencies:
- dependency-name: uvicorn
  dependency-version: 0.35.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-06 13:09:32 +02:00
dependabot[bot]
d823c7916f build(deps): bump fastapi[standard] from 0.115.13 to 0.115.14 (#608)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Bumps [fastapi[standard]](https://github.com/fastapi/fastapi) from 0.115.13 to 0.115.14.
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.115.13...0.115.14)

---
updated-dependencies:
- dependency-name: fastapi[standard]
  dependency-version: 0.115.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-01 11:26:38 +02:00
redmoon2711
ba4f5ecbb5 Optimization of the Inverter class to speed up scr calculation (#607)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
* fix(ElecPriceEnergyCharts): get history series, update docs

Signed-off-by: redmoon2711 <redmoon2711@gmx.de>

* feat(inverter): using lookup table for calculate scr

Signed-off-by: redmoon2711 <redmoon2711@gmx.de>

---------

Signed-off-by: redmoon2711 <redmoon2711@gmx.de>
2025-06-25 21:13:11 +02:00
redmoon2711
eb23123bba fix(ElecPriceEnergyCharts): get history series, update docs (#606)
Signed-off-by: redmoon2711 <redmoon2711@gmx.de>
2025-06-25 08:26:15 +02:00
dependabot[bot]
eacd9010e1 build(deps): bump pydantic-settings from 2.9.1 to 2.10.0 (#605)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Bumps [pydantic-settings](https://github.com/pydantic/pydantic-settings) from 2.9.1 to 2.10.0.
- [Release notes](https://github.com/pydantic/pydantic-settings/releases)
- [Commits](https://github.com/pydantic/pydantic-settings/compare/v2.9.1...2.10.0)

---
updated-dependencies:
- dependency-name: pydantic-settings
  dependency-version: 2.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-24 14:44:52 +02:00
redmoon2711
8c56410338 Add new electricity price provider: Energy-Charts #381 (#590)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
* feat(ElecPriceEnergyCharts): Add new electricity price provider: Energy-Charts

* feat(ElecPriceEnergyCharts): update data only if needed

* test(elecpriceforecast): add test for energycharts

* docs(predictions.md): add ElecPriceEnergyCharts Provider

Signed-off-by: redmoon2711 <redmoon2711@gmx.de>
2025-06-23 07:29:33 +02:00
dependabot[bot]
9e789e1786 build(deps): bump bokeh from 3.7.2 to 3.7.3 (#603)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Bumps [bokeh](https://github.com/bokeh/bokeh) from 3.7.2 to 3.7.3.
- [Changelog](https://github.com/bokeh/bokeh/blob/3.7.3/docs/CHANGELOG)
- [Commits](https://github.com/bokeh/bokeh/compare/3.7.2...3.7.3)

---
updated-dependencies:
- dependency-name: bokeh
  dependency-version: 3.7.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-22 00:33:06 +02:00
dependabot[bot]
ca19534e4e build(deps): bump numpy from 2.3.0 to 2.3.1 (#604)
Bumps [numpy](https://github.com/numpy/numpy) from 2.3.0 to 2.3.1.
- [Release notes](https://github.com/numpy/numpy/releases)
- [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst)
- [Commits](https://github.com/numpy/numpy/compare/v2.3.0...v2.3.1)

---
updated-dependencies:
- dependency-name: numpy
  dependency-version: 2.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-22 00:32:50 +02:00
dependabot[bot]
066fb89d6e build(deps-dev): bump pytest from 8.4.0 to 8.4.1 (#599)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.4.0 to 8.4.1.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.4.0...8.4.1)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 8.4.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-21 15:12:27 +02:00
dependabot[bot]
b533b396e7 build(deps): bump python-fasthtml from 0.12.19 to 0.12.20 (#600)
Bumps [python-fasthtml](https://github.com/AnswerDotAI/fasthtml) from 0.12.19 to 0.12.20.
- [Release notes](https://github.com/AnswerDotAI/fasthtml/releases)
- [Changelog](https://github.com/AnswerDotAI/fasthtml/blob/main/CHANGELOG.md)
- [Commits](https://github.com/AnswerDotAI/fasthtml/compare/0.12.19...0.12.20)

---
updated-dependencies:
- dependency-name: python-fasthtml
  dependency-version: 0.12.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-21 15:12:00 +02:00
dependabot[bot]
5fc86e517d build(deps): bump pvlib from 0.12.0 to 0.13.0 (#598)
Bumps [pvlib](https://github.com/pvlib/pvlib-python) from 0.12.0 to 0.13.0.
- [Release notes](https://github.com/pvlib/pvlib-python/releases)
- [Commits](https://github.com/pvlib/pvlib-python/compare/v0.12.0...v0.13.0)

---
updated-dependencies:
- dependency-name: pvlib
  dependency-version: 0.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-21 14:34:00 +02:00
dependabot[bot]
aa8520dfc3 build(deps): bump fastapi[standard] from 0.115.12 to 0.115.13 (#601)
Bumps [fastapi[standard]](https://github.com/fastapi/fastapi) from 0.115.12 to 0.115.13.
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.115.12...0.115.13)

---
updated-dependencies:
- dependency-name: fastapi[standard]
  dependency-version: 0.115.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-21 14:32:14 +02:00
dependabot[bot]
cb23999f5b build(deps-dev): bump mypy from 1.16.0 to 1.16.1 (#602)
Bumps [mypy](https://github.com/python/mypy) from 1.16.0 to 1.16.1.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.16.0...v1.16.1)

---
updated-dependencies:
- dependency-name: mypy
  dependency-version: 1.16.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-21 14:31:37 +02:00
dependabot[bot]
fac74ee92e build(deps-dev): bump types-requests (#594)
Bumps [types-requests](https://github.com/typeshed-internal/stub_uploader) from 2.32.0.20250602 to 2.32.4.20250611.
- [Commits](https://github.com/typeshed-internal/stub_uploader/commits)

---
updated-dependencies:
- dependency-name: types-requests
  dependency-version: 2.32.4.20250611
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-21 12:33:04 +02:00
dependabot[bot]
a0e5126d07 build(deps): bump numpy from 2.2.6 to 2.3.0 (#591)
Bumps [numpy](https://github.com/numpy/numpy) from 2.2.6 to 2.3.0.
- [Release notes](https://github.com/numpy/numpy/releases)
- [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst)
- [Commits](https://github.com/numpy/numpy/compare/v2.2.6...v2.3.0)

---
updated-dependencies:
- dependency-name: numpy
  dependency-version: 2.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-21 12:32:51 +02:00
dependabot[bot]
83a2b7171f build(deps-dev): bump pytest-cov from 6.1.1 to 6.2.1 (#592)
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 6.1.1 to 6.2.1.
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v6.1.1...v6.2.1)

---
updated-dependencies:
- dependency-name: pytest-cov
  dependency-version: 6.2.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-21 12:32:33 +02:00
dependabot[bot]
501c7e4005 build(deps): bump scikit-learn from 1.6.1 to 1.7.0 (#595)
Bumps [scikit-learn](https://github.com/scikit-learn/scikit-learn) from 1.6.1 to 1.7.0.
- [Release notes](https://github.com/scikit-learn/scikit-learn/releases)
- [Commits](https://github.com/scikit-learn/scikit-learn/compare/1.6.1...1.7.0)

---
updated-dependencies:
- dependency-name: scikit-learn
  dependency-version: 1.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-21 12:32:10 +02:00
dependabot[bot]
af3740ac49 build(deps): bump pydantic from 2.11.5 to 2.11.7 (#593)
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.11.5 to 2.11.7.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.11.5...v2.11.7)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-version: 2.11.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-21 11:18:07 +02:00
Bobby Noelte
bd38b3c5ef fix: logging, prediction update, multiple bugs (#584)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
* Fix logging configuration issues that made logging stop operation. Switch to Loguru
  logging (from Python logging). Enable console and file logging with different log levels.
  Add logging documentation.

* Fix logging configuration and EOS configuration out of sync. Added tracking support
  for nested value updates of Pydantic models. This used to update the logging configuration
  when the EOS configurationm for logging is changed. Should keep logging config and EOS
  config in sync as long as all changes to the EOS logging configuration are done by
  set_nested_value(), which is the case for the REST API.

* Fix energy management task looping endlessly after the second update when trying to update
  the last_update datetime.

* Fix get_nested_value() to correctly take values from the dicts in a Pydantic model instance.

* Fix usage of model classes instead of model instances in nested value access when evaluation
  the value type that is associated to each key.

* Fix illegal json format in prediction documentation for PVForecastAkkudoktor provider.

* Fix documentation qirks and add EOS Connect to integrations.

* Support deprecated fields in configuration in documentation generation and EOSdash.

* Enhance EOSdash demo to show BrightSky humidity data (that is often missing)

* Update documentation reference to German EOS installation videos.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2025-06-10 22:00:28 +02:00
dependabot[bot]
9d46f3c08e build(deps): bump pandas from 2.2.3 to 2.3.0 (#585)
Bumps [pandas](https://github.com/pandas-dev/pandas) from 2.2.3 to 2.3.0.
- [Release notes](https://github.com/pandas-dev/pandas/releases)
- [Commits](https://github.com/pandas-dev/pandas/compare/v2.2.3...v2.3.0)

---
updated-dependencies:
- dependency-name: pandas
  dependency-version: 2.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-10 08:36:18 +02:00
dependabot[bot]
94ebcbf6d2 build(deps): bump requests from 2.32.3 to 2.32.4 (#586)
Bumps [requests](https://github.com/psf/requests) from 2.32.3 to 2.32.4.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.32.3...v2.32.4)

---
updated-dependencies:
- dependency-name: requests
  dependency-version: 2.32.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-10 08:27:09 +02:00
dependabot[bot]
98738a16c9 build(deps-dev): bump pytest from 8.3.5 to 8.4.0 (#577)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.5 to 8.4.0.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.3.5...8.4.0)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 8.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-03 09:57:15 +02:00
dependabot[bot]
50fec138c9 build(deps-dev): bump types-requests (#576)
Bumps [types-requests](https://github.com/typeshed-internal/stub_uploader) from 2.32.0.20250515 to 2.32.0.20250602.
- [Commits](https://github.com/typeshed-internal/stub_uploader/commits)

---
updated-dependencies:
- dependency-name: types-requests
  dependency-version: 2.32.0.20250602
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-03 08:31:16 +02:00
dependabot[bot]
77997e2720 build(deps): bump uvicorn from 0.34.2 to 0.34.3 (#578)
Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.34.2 to 0.34.3.
- [Release notes](https://github.com/encode/uvicorn/releases)
- [Changelog](https://github.com/encode/uvicorn/blob/master/docs/release-notes.md)
- [Commits](https://github.com/encode/uvicorn/compare/0.34.2...0.34.3)

---
updated-dependencies:
- dependency-name: uvicorn
  dependency-version: 0.34.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-03 08:30:57 +02:00
Bobby Noelte
3421b2303b ci(ruff): add bandit checks (#575)
Added bandit checks to continuous integration.

Updated sources to pass bandit checks:
- replaced asserts
- added timeouts to requests
- added checks for process command execution
- changed to 127.0.0.1 as default IP address for EOS and EOSdash for security reasons

Added a rudimentary check for outdated config files.

BREAKING CHANGE: Default IP address for EOS and EOSdash changed to 127.0.0.1

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2025-06-03 08:30:37 +02:00
rompic
aa39ff475c fix: add required fields to example optimization request (#574)
* fix: add required fields to example optimization request
* fix: documentatin of parameters
2025-06-02 10:47:02 +02:00
dependabot[bot]
163c87ca3d build(deps-dev): bump pandas-stubs from 2.2.3.250308 to 2.2.3.250527 (#570)
Bumps [pandas-stubs](https://github.com/pandas-dev/pandas-stubs) from 2.2.3.250308 to 2.2.3.250527.
- [Changelog](https://github.com/pandas-dev/pandas-stubs/blob/main/docs/release_procedure.md)
- [Commits](https://github.com/pandas-dev/pandas-stubs/compare/v2.2.3.250308...v2.2.3.250527)

---
updated-dependencies:
- dependency-name: pandas-stubs
  dependency-version: 2.2.3.250527
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-31 11:17:54 +02:00
dependabot[bot]
32948dbf16 build(deps): bump python-fasthtml from 0.12.18 to 0.12.19 (#571)
Bumps [python-fasthtml](https://github.com/AnswerDotAI/fasthtml) from 0.12.18 to 0.12.19.
- [Release notes](https://github.com/AnswerDotAI/fasthtml/releases)
- [Changelog](https://github.com/AnswerDotAI/fasthtml/blob/main/CHANGELOG.md)
- [Commits](https://github.com/AnswerDotAI/fasthtml/compare/0.12.18...0.12.19)

---
updated-dependencies:
- dependency-name: python-fasthtml
  dependency-version: 0.12.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-31 11:17:37 +02:00
dependabot[bot]
6e7ef76f7e build(deps-dev): bump mypy from 1.15.0 to 1.16.0 (#572)
Bumps [mypy](https://github.com/python/mypy) from 1.15.0 to 1.16.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.15.0...v1.16.0)

---
updated-dependencies:
- dependency-name: mypy
  dependency-version: 1.16.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-30 22:12:33 +02:00
dependabot[bot]
f2ff3ef168 build(deps): bump monsterui from 1.0.20 to 1.0.21 (#573)
Bumps [monsterui](https://github.com/AnswerDotAI/MonsterUI) from 1.0.20 to 1.0.21.
- [Release notes](https://github.com/AnswerDotAI/MonsterUI/releases)
- [Changelog](https://github.com/AnswerDotAI/MonsterUI/blob/main/CHANGELOG.bak)
- [Commits](https://github.com/AnswerDotAI/MonsterUI/compare/1.0.20...1.0.21)

---
updated-dependencies:
- dependency-name: monsterui
  dependency-version: 1.0.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-30 22:11:42 +02:00
rompic
e413543222 fix: pvforecast fails when there is only a single plane (#569)
* fix: pvforecast fails when there is only a single plane

* fix: formatting

* fix: formatting

* fix: add type annotations

* add testdata and validation test for single plane

* fix: formatting
2025-05-30 21:55:45 +02:00
rompic
058356d1b8 fix: delete empty inverter from testdata optimize_input_2.json (#568) 2025-05-30 10:40:45 +02:00
Bobby Noelte
3ec36e0932 fix: azimuth setting of pvforecastakkudoktor provider (#567)
EOS now enforces the general azimuth definition as e.g. defined in ISO 19111:
north=0, east=90, south=180, west=270. This is the convention that is and was
in the EOS documentation.

As the PV forecast of akkudoktor.net follows a different convention
(north=+-180, east=-90, south=0, west=90) the values from EOS are now converted
before the request is sent to akkudoktor.net.

BREAKING CHANGE: Azimuth configurations that followed the PVForecastAkkudoktor convention
(north=+-180, east=-90, south=0, west=90) must be converted to the general azimuth definition:
north=0, east=90, south=180, west=270.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2025-05-28 20:42:43 +02:00
rompic
46e078fce1 chore: typos and consistant spelling of Home Assistant (#563) 2025-05-26 21:48:50 +02:00
dependabot[bot]
f88260dc3f build(deps-dev): bump pymarkdownlnt from 0.9.29 to 0.9.30 (#560)
Bumps [pymarkdownlnt](https://github.com/jackdewinter/pymarkdown) from 0.9.29 to 0.9.30.
- [Release notes](https://github.com/jackdewinter/pymarkdown/releases)
- [Changelog](https://github.com/jackdewinter/pymarkdown/blob/main/changelog.md)
- [Commits](https://github.com/jackdewinter/pymarkdown/compare/v0.9.29...v0.9.30)

---
updated-dependencies:
- dependency-name: pymarkdownlnt
  dependency-version: 0.9.30
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-25 22:37:20 +02:00
dependabot[bot]
289b96ac3d build(deps): bump pydantic from 2.11.4 to 2.11.5 (#559)
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.11.4 to 2.11.5.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.11.4...v2.11.5)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-version: 2.11.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-25 22:36:35 +02:00
Bobby Noelte
9959cb14a4 fix: BrightSky with None humidity data (#555)
Make the BrightSky weather forecast more robust for the case that BrightSky
does not provide relative humidity data for a certain location.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2025-05-22 11:15:48 +02:00
dependabot[bot]
ccd2f5307c build(deps): bump python-fasthtml from 0.12.16 to 0.12.18 (#554)
Bumps [python-fasthtml](https://github.com/AnswerDotAI/fasthtml) from 0.12.16 to 0.12.18.
- [Release notes](https://github.com/AnswerDotAI/fasthtml/releases)
- [Changelog](https://github.com/AnswerDotAI/fasthtml/blob/main/CHANGELOG.md)
- [Commits](https://github.com/AnswerDotAI/fasthtml/compare/0.12.16...0.12.18)

---
updated-dependencies:
- dependency-name: python-fasthtml
  dependency-version: 0.12.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 11:09:13 +02:00
dependabot[bot]
ce90a5bbca build(deps): bump numpy from 2.2.5 to 2.2.6 (#553)
Bumps [numpy](https://github.com/numpy/numpy) from 2.2.5 to 2.2.6.
- [Release notes](https://github.com/numpy/numpy/releases)
- [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst)
- [Commits](https://github.com/numpy/numpy/compare/v2.2.5...v2.2.6)

---
updated-dependencies:
- dependency-name: numpy
  dependency-version: 2.2.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 11:08:46 +02:00
dependabot[bot]
df4ab31040 build(deps): bump python-fasthtml from 0.12.15 to 0.12.16 (#550)
Bumps [python-fasthtml](https://github.com/AnswerDotAI/fasthtml) from 0.12.15 to 0.12.16.
- [Release notes](https://github.com/AnswerDotAI/fasthtml/releases)
- [Changelog](https://github.com/AnswerDotAI/fasthtml/blob/main/CHANGELOG.md)
- [Commits](https://github.com/AnswerDotAI/fasthtml/compare/0.12.15...0.12.16)

---
updated-dependencies:
- dependency-name: python-fasthtml
  dependency-version: 0.12.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-16 02:53:41 +02:00
dependabot[bot]
c4b1237ba6 build(deps-dev): bump types-requests (#549)
Bumps [types-requests](https://github.com/typeshed-internal/stub_uploader) from 2.32.0.20250328 to 2.32.0.20250515.
- [Commits](https://github.com/typeshed-internal/stub_uploader/commits)

---
updated-dependencies:
- dependency-name: types-requests
  dependency-version: 2.32.0.20250515
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-16 02:34:11 +02:00
dependabot[bot]
62f8aaffc5 build(deps): bump monsterui from 1.0.19 to 1.0.20 (#551)
Bumps [monsterui](https://github.com/AnswerDotAI/MonsterUI) from 1.0.19 to 1.0.20.
- [Release notes](https://github.com/AnswerDotAI/MonsterUI/releases)
- [Changelog](https://github.com/AnswerDotAI/MonsterUI/blob/main/CHANGELOG.bak)
- [Commits](https://github.com/AnswerDotAI/MonsterUI/compare/1.0.19...1.0.20)

---
updated-dependencies:
- dependency-name: monsterui
  dependency-version: 1.0.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-16 02:33:33 +02:00
dependabot[bot]
c6aed8d847 build(deps): bump numpydantic from 1.6.8 to 1.6.9 (#548)
Bumps [numpydantic](https://github.com/p2p-ld/numpydantic) from 1.6.8 to 1.6.9.
- [Release notes](https://github.com/p2p-ld/numpydantic/releases)
- [Changelog](https://github.com/p2p-ld/numpydantic/blob/main/docs/changelog.md)
- [Commits](https://github.com/p2p-ld/numpydantic/compare/v1.6.8...v1.6.9)

---
updated-dependencies:
- dependency-name: numpydantic
  dependency-version: 1.6.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-13 08:16:24 +02:00
SchaafAlexander
6c1f728fdf Activation of venv is needed (#546) 2025-05-10 20:30:12 +02:00
dependabot[bot]
481ebd24d7 build(deps): bump matplotlib from 3.10.1 to 3.10.3 (#545)
Bumps [matplotlib](https://github.com/matplotlib/matplotlib) from 3.10.1 to 3.10.3.
- [Release notes](https://github.com/matplotlib/matplotlib/releases)
- [Commits](https://github.com/matplotlib/matplotlib/compare/v3.10.1...v3.10.3)

---
updated-dependencies:
- dependency-name: matplotlib
  dependency-version: 3.10.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-10 02:44:41 +02:00
dependabot[bot]
f1ba9fc2e0 build(deps): bump platformdirs from 4.3.7 to 4.3.8 (#544)
Bumps [platformdirs](https://github.com/tox-dev/platformdirs) from 4.3.7 to 4.3.8.
- [Release notes](https://github.com/tox-dev/platformdirs/releases)
- [Changelog](https://github.com/tox-dev/platformdirs/blob/main/CHANGES.rst)
- [Commits](https://github.com/tox-dev/platformdirs/compare/4.3.7...4.3.8)

---
updated-dependencies:
- dependency-name: platformdirs
  dependency-version: 4.3.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-10 02:44:21 +02:00
dependabot[bot]
9509451b6c Bump deap from 1.4.2 to 1.4.3 (#541)
Bumps [deap](https://github.com/DEAP/deap) from 1.4.2 to 1.4.3.
- [Changelog](https://github.com/DEAP/deap/blob/master/doc/releases.rst)
- [Commits](https://github.com/DEAP/deap/commits)

---
updated-dependencies:
- dependency-name: deap
  dependency-version: 1.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-06 11:16:14 +02:00
dependabot[bot]
cecaf4cd16 Bump python-fasthtml from 0.12.14 to 0.12.15 (#542)
Bumps [python-fasthtml](https://github.com/AnswerDotAI/fasthtml) from 0.12.14 to 0.12.15.
- [Release notes](https://github.com/AnswerDotAI/fasthtml/releases)
- [Changelog](https://github.com/AnswerDotAI/fasthtml/blob/main/CHANGELOG.md)
- [Commits](https://github.com/AnswerDotAI/fasthtml/compare/0.12.14...0.12.15)

---
updated-dependencies:
- dependency-name: python-fasthtml
  dependency-version: 0.12.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-06 11:15:42 +02:00
Marco
f8c9fc191a chore: Typos 2025-05-06 09:55:40 +02:00
dependabot[bot]
0f9ad6bf80 Bump pydantic from 2.11.3 to 2.11.4 (#539)
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.11.3 to 2.11.4.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.11.3...v2.11.4)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-version: 2.11.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-30 23:40:28 +02:00
Bobby Noelte
dac089b320 update(pydantic): Bump pydantic to 2.11.3 (#538)
Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2025-04-30 23:05:44 +02:00
dependabot[bot]
2bfe646996 Bump cachebox from 5.0.0 to 5.0.1 (#536)
Bumps [cachebox](https://github.com/awolverp/cachebox) from 5.0.0 to 5.0.1.
- [Release notes](https://github.com/awolverp/cachebox/releases)
- [Changelog](https://github.com/awolverp/cachebox/blob/main/CHANGELOG.md)
- [Commits](https://github.com/awolverp/cachebox/compare/v5.0.0...v5.0.1)

---
updated-dependencies:
- dependency-name: cachebox
  dependency-version: 5.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-29 10:07:15 +02:00
Dominique Lasserre
c89e8e95fa fix: Catch optimize error and return error message. (#534) 2025-04-23 16:26:35 +02:00
Dominique Lasserre
7ade15e9e3 fix: Circular runtime import Closes #533 (#535) 2025-04-23 16:26:04 +02:00
dependabot[bot]
8b880299b8 Bump cachebox from 4.5.3 to 5.0.0 (#530)
Bumps [cachebox](https://github.com/awolverp/cachebox) from 4.5.3 to 5.0.0.
- [Release notes](https://github.com/awolverp/cachebox/releases)
- [Changelog](https://github.com/awolverp/cachebox/blob/main/CHANGELOG.md)
- [Commits](https://github.com/awolverp/cachebox/compare/v4.5.3...v5.0.0)

---
updated-dependencies:
- dependency-name: cachebox
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-22 08:45:05 +02:00
dependabot[bot]
e7f83e1bfd Bump numpy from 2.2.4 to 2.2.5 (#531)
Bumps [numpy](https://github.com/numpy/numpy) from 2.2.4 to 2.2.5.
- [Release notes](https://github.com/numpy/numpy/releases)
- [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst)
- [Commits](https://github.com/numpy/numpy/compare/v2.2.4...v2.2.5)

---
updated-dependencies:
- dependency-name: numpy
  dependency-version: 2.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-22 08:37:11 +02:00
dependabot[bot]
52eb4c195f Bump pydantic-settings from 2.8.1 to 2.9.1 (#532)
Bumps [pydantic-settings](https://github.com/pydantic/pydantic-settings) from 2.8.1 to 2.9.1.
- [Release notes](https://github.com/pydantic/pydantic-settings/releases)
- [Commits](https://github.com/pydantic/pydantic-settings/compare/v2.8.1...v2.9.1)

---
updated-dependencies:
- dependency-name: pydantic-settings
  dependency-version: 2.9.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-22 08:35:49 +02:00
Christian Hohlfeld
e2a0b8b564 fix(docker): enable BuildKit to support --mount (closes #493) 2025-04-22 01:20:46 +02:00
dependabot[bot]
f7cc43f68c Bump monsterui from 1.0.18 to 1.0.19 (#526)
Bumps [monsterui](https://github.com/AnswerDotAI/MonsterUI) from 1.0.18 to 1.0.19.
- [Release notes](https://github.com/AnswerDotAI/MonsterUI/releases)
- [Changelog](https://github.com/AnswerDotAI/MonsterUI/blob/main/CHANGELOG.bak)
- [Commits](https://github.com/AnswerDotAI/MonsterUI/compare/1.0.18...1.0.19)

---
updated-dependencies:
- dependency-name: monsterui
  dependency-version: 1.0.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-22 00:58:28 +02:00
dependabot[bot]
60838ac029 Bump python-fasthtml from 0.12.12 to 0.12.14 (#527)
Bumps [python-fasthtml](https://github.com/AnswerDotAI/fasthtml) from 0.12.12 to 0.12.14.
- [Release notes](https://github.com/AnswerDotAI/fasthtml/releases)
- [Changelog](https://github.com/AnswerDotAI/fasthtml/blob/main/CHANGELOG.md)
- [Commits](https://github.com/AnswerDotAI/fasthtml/compare/0.12.12...0.12.14)

---
updated-dependencies:
- dependency-name: python-fasthtml
  dependency-version: 0.12.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-22 00:44:06 +02:00
dependabot[bot]
cae494458c Bump uvicorn from 0.34.1 to 0.34.2 (#528)
Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.34.1 to 0.34.2.
- [Release notes](https://github.com/encode/uvicorn/releases)
- [Changelog](https://github.com/encode/uvicorn/blob/master/docs/release-notes.md)
- [Commits](https://github.com/encode/uvicorn/compare/0.34.1...0.34.2)

---
updated-dependencies:
- dependency-name: uvicorn
  dependency-version: 0.34.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-22 00:43:42 +02:00
dependabot[bot]
68e1259ffa Bump pendulum from 3.0.0 to 3.1.0 (#529)
Bumps [pendulum](https://github.com/sdispater/pendulum) from 3.0.0 to 3.1.0.
- [Release notes](https://github.com/sdispater/pendulum/releases)
- [Changelog](https://github.com/python-pendulum/pendulum/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sdispater/pendulum/compare/3.0.0...3.1.0)

---
updated-dependencies:
- dependency-name: pendulum
  dependency-version: 3.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-22 00:43:19 +02:00
Christian Heinrich Hohlfeld
3c12e99970 fix: mitigate ReDoS in to_duration via max input length check (closes #494) (#523) 2025-04-22 00:16:33 +02:00
Christian Heinrich Hohlfeld
63962343d9 ci(docker): add :latest tag on default branch builds (#522) Fixes #499 2025-04-21 19:27:33 +02:00
Bobby Noelte
1e1b7540f3 fix: relax stale issue/pr handling
Wait 90 days until stale indication, wait another 30 days until closing.
Excempt "enhancement" and "feature request" issues from stale handling.
Excempt "in progress" pull reuquests from stale handling.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2025-04-20 08:07:44 +02:00
dependabot[bot]
a39e0a77a7 Bump uvicorn from 0.34.0 to 0.34.1 (#519)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.34.0 to 0.34.1.
- [Release notes](https://github.com/encode/uvicorn/releases)
- [Changelog](https://github.com/encode/uvicorn/blob/master/docs/release-notes.md)
- [Commits](https://github.com/encode/uvicorn/compare/0.34.0...0.34.1)

---
updated-dependencies:
- dependency-name: uvicorn
  dependency-version: 0.34.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-16 09:35:28 +02:00
dependabot[bot]
57f8d3ac17 Bump cachebox from 4.4.2 to 4.5.3 (#514)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
Bumps [cachebox](https://github.com/awolverp/cachebox) from 4.4.2 to 4.5.3.
- [Release notes](https://github.com/awolverp/cachebox/releases)
- [Changelog](https://github.com/awolverp/cachebox/blob/main/CHANGELOG.md)
- [Commits](https://github.com/awolverp/cachebox/compare/v4.4.2...v4.5.3)

---
updated-dependencies:
- dependency-name: cachebox
  dependency-version: 4.5.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-09 11:17:57 +02:00
dependabot[bot]
dc2dbd07b3 Bump bokeh from 3.7.0 to 3.7.2 (#516)
Bumps [bokeh](https://github.com/bokeh/bokeh) from 3.7.0 to 3.7.2.
- [Changelog](https://github.com/bokeh/bokeh/blob/3.7.2/docs/CHANGELOG)
- [Commits](https://github.com/bokeh/bokeh/compare/3.7.0...3.7.2)

---
updated-dependencies:
- dependency-name: bokeh
  dependency-version: 3.7.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-09 11:17:41 +02:00
dependabot[bot]
c8e9b5d5c8 Bump monsterui from 1.0.11 to 1.0.18 (#517)
Bumps [monsterui](https://github.com/AnswerDotAI/MonsterUI) from 1.0.11 to 1.0.18.
- [Release notes](https://github.com/AnswerDotAI/MonsterUI/releases)
- [Changelog](https://github.com/AnswerDotAI/MonsterUI/blob/main/CHANGELOG.bak)
- [Commits](https://github.com/AnswerDotAI/MonsterUI/compare/1.0.11...1.0.18)

---
updated-dependencies:
- dependency-name: monsterui
  dependency-version: 1.0.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-09 11:16:51 +02:00
dependabot[bot]
876ea65140 Bump pytest-cov from 6.0.0 to 6.1.1 (#518)
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 6.0.0 to 6.1.1.
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v6.0.0...v6.1.1)

---
updated-dependencies:
- dependency-name: pytest-cov
  dependency-version: 6.1.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-09 11:16:26 +02:00
Dominique Lasserre
cc5fb060ed Merge remote-tracking branch 'origin/main' into feature/config-nested 2025-04-09 00:24:50 +02:00
dependabot[bot]
95c99759ff Bump python-fasthtml from 0.12.6 to 0.12.12 (#508)
Bumps [python-fasthtml](https://github.com/AnswerDotAI/fasthtml) from 0.12.6 to 0.12.12.
- [Release notes](https://github.com/AnswerDotAI/fasthtml/releases)
- [Changelog](https://github.com/AnswerDotAI/fasthtml/blob/main/CHANGELOG.md)
- [Commits](https://github.com/AnswerDotAI/fasthtml/compare/0.12.6...0.12.12)

---
updated-dependencies:
- dependency-name: python-fasthtml
  dependency-version: 0.12.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-09 00:24:36 +02:00
Bobby Noelte
9626bfb32b ci: lint commit messages (#510)
Lint commit messages using gitlint in pre-commit.
Gitlint enforces rules that are configured by .gitlint.

The checks enforce the [`Conventional Commits`](https://www.conventionalcommits.org)
commit message style.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2025-04-07 22:23:35 +02:00
dependabot[bot]
dc7df99114 Bump python-fasthtml from 0.12.6 to 0.12.12 (#508)
Bumps [python-fasthtml](https://github.com/AnswerDotAI/fasthtml) from 0.12.6 to 0.12.12.
- [Release notes](https://github.com/AnswerDotAI/fasthtml/releases)
- [Changelog](https://github.com/AnswerDotAI/fasthtml/blob/main/CHANGELOG.md)
- [Commits](https://github.com/AnswerDotAI/fasthtml/compare/0.12.6...0.12.12)

---
updated-dependencies:
- dependency-name: python-fasthtml
  dependency-version: 0.12.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-07 17:26:00 +02:00
dependabot[bot]
9f60354e0d Bump pytest-cov from 6.0.0 to 6.1.1 (#509)
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 6.0.0 to 6.1.1.
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v6.0.0...v6.1.1)

---
updated-dependencies:
- dependency-name: pytest-cov
  dependency-version: 6.1.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-07 17:25:02 +02:00
Dominique Lasserre
0f7837458f Merge remote-tracking branch 'origin/main' into feature/config-nested 2025-04-06 14:51:07 +02:00
Bobby Noelte
4bce51430e Close stale pull requests/ issues after 60 days (#501)
Add GitHub workflow to close requests/issues after 60 days of inactivity.
Requests/ issues may be blocked from closing by adding the In progress label.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2025-04-06 14:46:52 +02:00
dependabot[bot]
3790a405dc Bump python-fasthtml from 0.12.4 to 0.12.6 (#502)
Bumps [python-fasthtml](https://github.com/AnswerDotAI/fasthtml) from 0.12.4 to 0.12.6.
- [Release notes](https://github.com/AnswerDotAI/fasthtml/releases)
- [Changelog](https://github.com/AnswerDotAI/fasthtml/blob/main/CHANGELOG.md)
- [Commits](https://github.com/AnswerDotAI/fasthtml/compare/0.12.4...0.12.6)

---
updated-dependencies:
- dependency-name: python-fasthtml
  dependency-version: 0.12.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-06 14:46:52 +02:00
dependabot[bot]
4ea89b3a5a Bump timezonefinder from 6.5.8 to 6.5.9 (#504)
Bumps [timezonefinder](https://github.com/jannikmi/timezonefinder) from 6.5.8 to 6.5.9.
- [Release notes](https://github.com/jannikmi/timezonefinder/releases)
- [Changelog](https://github.com/jannikmi/timezonefinder/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jannikmi/timezonefinder/compare/6.5.8...6.5.9)

---
updated-dependencies:
- dependency-name: timezonefinder
  dependency-version: 6.5.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-06 14:46:52 +02:00
dependabot[bot]
9979b22a74 Bump types-requests from 2.32.0.20250306 to 2.32.0.20250328 (#505)
Bumps [types-requests](https://github.com/python/typeshed) from 2.32.0.20250306 to 2.32.0.20250328.
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-requests
  dependency-version: 2.32.0.20250328
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-06 14:46:52 +02:00
Ikko Eltociear Ashimine
8f30d97f86 chore: update weatherclearoutside.py (#496)
reponse -> response
2025-04-06 14:46:52 +02:00
Yunus AYDIN
87ebbf0f08 Fix Cross Site Scripting Issue (#497) 2025-04-06 14:46:52 +02:00
dependabot[bot]
8bdad48823 Bump fastapi[standard] from 0.115.11 to 0.115.12 (#492)
Bumps [fastapi[standard]](https://github.com/fastapi/fastapi) from 0.115.11 to 0.115.12.
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.115.11...0.115.12)

---
updated-dependencies:
- dependency-name: fastapi[standard]
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-06 14:46:52 +02:00
thiloms
64732e26e2 CONTRIBUTING.md fix documentation link (404 error) (#484)
Fix take over the same link as in README.md. The current link will end up in HTTP error 404 (not found).
2025-04-06 14:46:52 +02:00
Eric
dd114eee69 Conceptual documentation (#463)
Added new instruction.md, changed index.md accordingly and deleted the no longer needed about.md of new documentation structure.
Refinement of differences to other solutions and features of EOS.

Co-authored-by: Eric Hirsch <git@familie-hirsch.net>
2025-04-06 14:46:52 +02:00
Dominique Lasserre
5eb6d84572 Dockerfile: Set default for EOS_SERVER__EOSDASH_SESSKEY Closes #447 (#467)
* This allows to start the container without any extra settings
   (potentially unsafe).
   It is recommended to set EOS_SERVER__EOSDASH_SESSKEY.
2025-04-06 14:46:52 +02:00
Dominique Lasserre
c46d13731d docker-compose: Expose EOSdash port Closes #447
* Fixes direct EOSdash access on Windows localhost:8504 (required for
   redirect).
2025-04-06 14:46:52 +02:00
Dominique Lasserre
00229c39e4 visualize.py: Support variable remuneration Closes #451 (#459) 2025-04-06 14:46:52 +02:00
Dominique Lasserre
a95d6c939a Windows: Fix EOSdash startup Closes #436 #447 (#450)
* On Windows use 127.0.0.1 as default config host (model defaults) and
   addionally redirect 0.0.0.0 to localhost on Windows (because default
   config file still has 0.0.0.0).
   Use 0.0.0.0 as default otherwise (e.g. Linux/Docker) to allow EOS
   being accessible on local network (not just same host).
   Note: Docs generation on Windows is incompatible with the Github
   pipeline tests. Address this in the nested-config feature branch.
 * Update install/startup instructions as package installation is
   required atm and Docker on Windows has to be accessed at localhost or
   127.0.0.1 even though the server log says 0.0.0.0 (which is required
   to be available outside the container).
 * Fix EOSdash startup with read_only: true (support session key via
   EOS_SERVER__EOSDASH_SESSKEY variable). Backport of feature branch.
 * Remove root_path, causing Windows to fail load swagger UI (/docs).
2025-04-06 14:46:52 +02:00
celle1234
36d794c1c4 visualize: fix timestamps on diagrams (#430) Closes #387
* visualize: fix timestamps on diagrams
* set start time in all graphs to the same beginning hour

---------

Co-authored-by: Normann <github@koldrack.com>
2025-04-06 14:46:43 +02:00
Dominique Lasserre
84598c592c README.md: Add some system requirements (#438) 2025-04-06 12:47:34 +02:00
Theo Weiss
aee4ca2a4f remove excess double quotes in Makefile (#437) 2025-04-06 12:45:31 +02:00
Bobby Noelte
0bda5ba4cc EOSdash: Improve PV forecast configuration. (#500)
* Allow to configure planes and configuration values of planes separatedly.

Make single configuration values for planes explicitly available for configuration.
Still allows to also configure a plane by a whole plane value struct.

* Enhance admin page by file import and export of the EOS configuration

The actual EOS configuration can now be exported to the EOSdash server.
From there it can be also imported. For security reasons only import and export
from/ to a predefined directory on the EOSdash server is possible.

* Improve handling of nested value pathes in pydantic models.

Added separate methods for nested path access (get_nested_value, set_nested_value).
On value setting the missing fields along the nested path are now added automatically
and initialized with default values. Nested path access was before restricted to the
EOS configuration and is now part of the pydantic base model.

* Makefile

Add new target to run rests as CI does on Github. Improve target docs.

* Datetimeutil tests

Prolong acceptable time difference for comparison of approximately equal times in tests.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2025-04-05 13:08:12 +02:00
Bobby Noelte
b4bc3d0eb2 Close stale pull requests/ issues after 60 days (#501)
Add GitHub workflow to close requests/issues after 60 days of inactivity.
Requests/ issues may be blocked from closing by adding the In progress label.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2025-04-01 12:44:56 +02:00
dependabot[bot]
a1b19a54d7 Bump python-fasthtml from 0.12.4 to 0.12.6 (#502)
Bumps [python-fasthtml](https://github.com/AnswerDotAI/fasthtml) from 0.12.4 to 0.12.6.
- [Release notes](https://github.com/AnswerDotAI/fasthtml/releases)
- [Changelog](https://github.com/AnswerDotAI/fasthtml/blob/main/CHANGELOG.md)
- [Commits](https://github.com/AnswerDotAI/fasthtml/compare/0.12.4...0.12.6)

---
updated-dependencies:
- dependency-name: python-fasthtml
  dependency-version: 0.12.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-01 12:44:27 +02:00
dependabot[bot]
4adb408722 Bump timezonefinder from 6.5.8 to 6.5.9 (#504)
Bumps [timezonefinder](https://github.com/jannikmi/timezonefinder) from 6.5.8 to 6.5.9.
- [Release notes](https://github.com/jannikmi/timezonefinder/releases)
- [Changelog](https://github.com/jannikmi/timezonefinder/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jannikmi/timezonefinder/compare/6.5.8...6.5.9)

---
updated-dependencies:
- dependency-name: timezonefinder
  dependency-version: 6.5.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-01 12:44:01 +02:00
dependabot[bot]
dba6e36f96 Bump types-requests from 2.32.0.20250306 to 2.32.0.20250328 (#505)
Bumps [types-requests](https://github.com/python/typeshed) from 2.32.0.20250306 to 2.32.0.20250328.
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-requests
  dependency-version: 2.32.0.20250328
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-01 12:43:27 +02:00
Ikko Eltociear Ashimine
83b6bdbdc6 chore: update weatherclearoutside.py (#496)
reponse -> response
2025-03-29 00:39:30 +01:00
Yunus AYDIN
2468efe604 Fix Cross Site Scripting Issue (#497)
* Update eos.py

* ruff format

* ruff format
2025-03-27 22:22:18 +01:00
Bobby Noelte
e6a8c0508e Fix negative values load mean adjusted (#491)
* Fix negativ values in load_mean_adjusted

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2025-03-27 21:57:26 +01:00
Bobby Noelte
7aaf193682 EOSdash: Enable EOS configuration by EOSdash. (#477)
Improve config page to edit actual configuration used by EOS.
Add admin page to save the actual configuration to the configuration file.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2025-03-27 21:53:01 +01:00
dependabot[bot]
7734c9c32f Bump fastapi[standard] from 0.115.11 to 0.115.12 (#492)
Bumps [fastapi[standard]](https://github.com/fastapi/fastapi) from 0.115.11 to 0.115.12.
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.115.11...0.115.12)

---
updated-dependencies:
- dependency-name: fastapi[standard]
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-25 08:16:20 +01:00
Normann
61c5efc74f Update requirements.txt for dev branch 2025-03-24 08:04:12 +01:00
Normann
2c1f16a3fb Update requirements for dev branch (#488)
* Update requirements

* Update requirements-dev.txt

* Update pyproject.toml

* Update README.md
2025-03-23 22:31:49 +01:00
dependabot[bot]
c72051a08e Bump sphinx from 8.1.3 to 8.2.3 (#476)
Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 8.1.3 to 8.2.3.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES.rst)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v8.1.3...v8.2.3)

---
updated-dependencies:
- dependency-name: sphinx
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-23 22:14:51 +01:00
Normann
60bd320fbc Python req. to 3.11 for sphinx update (#487)
* Python req. to 3.11 for sphinx update

* Update pyproject.toml
2025-03-23 22:09:02 +01:00
dependabot[bot]
600e332aae Bump platformdirs from 4.3.6 to 4.3.7 (#485)
Bumps [platformdirs](https://github.com/tox-dev/platformdirs) from 4.3.6 to 4.3.7.
- [Release notes](https://github.com/tox-dev/platformdirs/releases)
- [Changelog](https://github.com/tox-dev/platformdirs/blob/main/CHANGES.rst)
- [Commits](https://github.com/tox-dev/platformdirs/compare/4.3.6...4.3.7)

---
updated-dependencies:
- dependency-name: platformdirs
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-23 21:14:34 +01:00
dependabot[bot]
6a51de04da Bump pvlib from 0.11.2 to 0.12.0 (#486)
Bumps [pvlib](https://github.com/pvlib/pvlib-python) from 0.11.2 to 0.12.0.
- [Release notes](https://github.com/pvlib/pvlib-python/releases)
- [Commits](https://github.com/pvlib/pvlib-python/compare/v0.11.2...v0.12.0)

---
updated-dependencies:
- dependency-name: pvlib
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-23 21:08:51 +01:00
dependabot[bot]
64c8415714 Bump pandas-stubs from 2.2.3.241126 to 2.2.3.250308 (#479)
Bumps [pandas-stubs](https://github.com/pandas-dev/pandas-stubs) from 2.2.3.241126 to 2.2.3.250308.
- [Changelog](https://github.com/pandas-dev/pandas-stubs/blob/main/docs/release_procedure.md)
- [Commits](https://github.com/pandas-dev/pandas-stubs/compare/v2.2.3.241126...v2.2.3.250308)

---
updated-dependencies:
- dependency-name: pandas-stubs
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-23 21:01:48 +01:00
dependabot[bot]
52f8b015b1 Bump numpy from 2.2.3 to 2.2.4 (#483)
Bumps [numpy](https://github.com/numpy/numpy) from 2.2.3 to 2.2.4.
- [Release notes](https://github.com/numpy/numpy/releases)
- [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst)
- [Commits](https://github.com/numpy/numpy/compare/v2.2.3...v2.2.4)

---
updated-dependencies:
- dependency-name: numpy
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-23 21:01:33 +01:00
thiloms
a4adb07ebf CONTRIBUTING.md fix documentation link (404 error) (#484)
Fix take over the same link as in README.md. The current link will end up in HTTP error 404 (not found).
2025-03-23 13:52:15 +01:00
dependabot[bot]
b69bbe897f Bump numpydantic from 1.6.7 to 1.6.8 (#480)
Bumps [numpydantic](https://github.com/p2p-ld/numpydantic) from 1.6.7 to 1.6.8.
- [Release notes](https://github.com/p2p-ld/numpydantic/releases)
- [Changelog](https://github.com/p2p-ld/numpydantic/blob/main/docs/changelog.md)
- [Commits](https://github.com/p2p-ld/numpydantic/compare/v1.6.7...v1.6.8)

---
updated-dependencies:
- dependency-name: numpydantic
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-23 13:51:06 +01:00
dependabot[bot]
0a7420c42b Bump types-requests from 2.32.0.20250301 to 2.32.0.20250306 (#478)
Bumps [types-requests](https://github.com/python/typeshed) from 2.32.0.20250301 to 2.32.0.20250306.
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-requests
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-23 13:49:47 +01:00
Eric
b22b5ee651 Conceptual documentation (#463)
Added new instruction.md, changed index.md accordingly and deleted the no longer needed about.md of new documentation structure.
Refinement of differences to other solutions and features of EOS.

Co-authored-by: Eric Hirsch <git@familie-hirsch.net>
2025-03-23 13:27:40 +01:00
dependabot[bot]
9b4ec74823 Bump pytest from 8.3.4 to 8.3.5 (#475)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.4 to 8.3.5.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.3.4...8.3.5)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-05 10:32:48 +01:00
dependabot[bot]
1f30d4e403 Bump python-fasthtml from 0.12.1 to 0.12.4 (#470)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Bumps [python-fasthtml](https://github.com/AnswerDotAI/fasthtml) from 0.12.1 to 0.12.4.
- [Release notes](https://github.com/AnswerDotAI/fasthtml/releases)
- [Changelog](https://github.com/AnswerDotAI/fasthtml/blob/main/CHANGELOG.md)
- [Commits](https://github.com/AnswerDotAI/fasthtml/compare/0.12.1...0.12.4)

---
updated-dependencies:
- dependency-name: python-fasthtml
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-02 11:10:23 +01:00
dependabot[bot]
b563bbbd98 Bump matplotlib from 3.10.0 to 3.10.1 (#471)
Bumps [matplotlib](https://github.com/matplotlib/matplotlib) from 3.10.0 to 3.10.1.
- [Release notes](https://github.com/matplotlib/matplotlib/releases)
- [Commits](https://github.com/matplotlib/matplotlib/compare/v3.10.0...v3.10.1)

---
updated-dependencies:
- dependency-name: matplotlib
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-02 11:04:30 +01:00
dependabot[bot]
8422a5c9d8 Bump types-requests from 2.32.0.20241016 to 2.32.0.20250301 (#473)
Bumps [types-requests](https://github.com/python/typeshed) from 2.32.0.20241016 to 2.32.0.20250301.
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-requests
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-02 10:58:46 +01:00
dependabot[bot]
bec5c2cbda Bump fastapi[standard] from 0.115.8 to 0.115.11 (#472)
Bumps [fastapi[standard]](https://github.com/fastapi/fastapi) from 0.115.8 to 0.115.11.
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.115.8...0.115.11)

---
updated-dependencies:
- dependency-name: fastapi[standard]
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-02 10:57:57 +01:00
Dominique Lasserre
d2136f1447 Dockerfile: Set default for EOS_SERVER__EOSDASH_SESSKEY Closes #447 (#467)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
* This allows to start the container without any extra settings
   (potentially unsafe).
   It is recommended to set EOS_SERVER__EOSDASH_SESSKEY.
2025-02-23 16:17:54 +01:00
Dominique Lasserre
20621aa626 docker-compose: Expose EOSdash port Closes #447
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
* Fixes direct EOSdash access on Windows localhost:8504 (required for
   redirect).
2025-02-18 07:08:14 +01:00
Dominique Lasserre
76b5ec3638 visualize.py: Support variable remuneration Closes #451 (#459)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
2025-02-16 11:06:31 +01:00
dependabot[bot]
d912561bfb Bump numpy from 2.2.2 to 2.2.3 (#456)
Some checks are pending
docker-build / platform-excludes (push) Waiting to run
docker-build / build (push) Blocked by required conditions
docker-build / merge (push) Blocked by required conditions
pre-commit / pre-commit (push) Waiting to run
Run Pytest on Pull Request / test (push) Waiting to run
Bumps [numpy](https://github.com/numpy/numpy) from 2.2.2 to 2.2.3.
- [Release notes](https://github.com/numpy/numpy/releases)
- [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst)
- [Commits](https://github.com/numpy/numpy/compare/v2.2.2...v2.2.3)

---
updated-dependencies:
- dependency-name: numpy
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-15 13:32:48 +01:00
dependabot[bot]
5907c94a2e Bump myst-parser from 4.0.0 to 4.0.1 (#455)
Bumps [myst-parser](https://github.com/executablebooks/MyST-Parser) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/executablebooks/MyST-Parser/releases)
- [Changelog](https://github.com/executablebooks/MyST-Parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/executablebooks/MyST-Parser/compare/v4.0.0...v4.0.1)

---
updated-dependencies:
- dependency-name: myst-parser
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-15 13:29:34 +01:00
Bobby Noelte
7b9b58f1e0 Add Markdown linter
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Add Markdown linter (pymarkdown) to pre-commit.
Adapt current markdown files to fulfill linter rules.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2025-02-13 12:10:47 +01:00
Dennis
c87bf2e4fc EOF issue in "optimize" documentation 2025-02-13 12:10:47 +01:00
Dennis
7773c4c2c9 Initial "optimize" documentation 2025-02-13 12:10:47 +01:00
Dominique Lasserre
b380624c9f Windows: Fix EOSdash startup Closes #436 #447 (#450)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
* On Windows use 127.0.0.1 as default config host (model defaults) and
   addionally redirect 0.0.0.0 to localhost on Windows (because default
   config file still has 0.0.0.0).
   Use 0.0.0.0 as default otherwise (e.g. Linux/Docker) to allow EOS
   being accessible on local network (not just same host).
   Note: Docs generation on Windows is incompatible with the Github
   pipeline tests. Address this in the nested-config feature branch.
 * Update install/startup instructions as package installation is
   required atm and Docker on Windows has to be accessed at localhost or
   127.0.0.1 even though the server log says 0.0.0.0 (which is required
   to be available outside the container).
 * Fix EOSdash startup with read_only: true (support session key via
   EOS_SERVER__EOSDASH_SESSKEY variable). Backport of feature branch.
 * Remove root_path, causing Windows to fail load swagger UI (/docs).
2025-02-10 00:38:35 +01:00
dependabot[bot]
caed880672 Bump mypy from 1.13.0 to 1.15.0 (#449)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Bumps [mypy](https://github.com/python/mypy) from 1.13.0 to 1.15.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.13.0...v1.15.0)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-08 01:08:17 +01:00
celle1234
6cc9a5fd44 visualize: fix timestamps on diagrams (#430) Closes #387
* visualize: fix timestamps on diagrams
* set start time in all graphs to the same beginning hour

---------

Co-authored-by: Normann <github@koldrack.com>
2025-02-08 00:47:21 +01:00
dependabot[bot]
80a4079bbf Bump fastapi[standard] from 0.115.7 to 0.115.8 (#442)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Bumps [fastapi[standard]](https://github.com/fastapi/fastapi) from 0.115.7 to 0.115.8.
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.115.7...0.115.8)

---
updated-dependencies:
- dependency-name: fastapi[standard]
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-04 08:30:54 +01:00
dependabot[bot]
cc687b140f Bump python-fasthtml from 0.12.0 to 0.12.1 (#441)
Bumps [python-fasthtml](https://github.com/AnswerDotAI/fasthtml) from 0.12.0 to 0.12.1.
- [Release notes](https://github.com/AnswerDotAI/fasthtml/releases)
- [Changelog](https://github.com/AnswerDotAI/fasthtml/blob/main/CHANGELOG.md)
- [Commits](https://github.com/AnswerDotAI/fasthtml/compare/0.12.0...0.12.1)

---
updated-dependencies:
- dependency-name: python-fasthtml
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-04 08:25:09 +01:00
Dominique Lasserre
29cf3a3174 README.md: Add some system requirements (#438)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
2025-02-01 20:46:00 +01:00
Theo Weiss
837595de56 remove excess double quotes in Makefile (#437) 2025-02-01 16:41:24 +01:00
Normann
1a2da7636b Data prefetch for ems (#418)
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
* Pre-fetch data

* maintanance and extend tests

* comment clean up

* nansum usage (to be save)
2025-01-26 18:29:26 +01:00
dependabot[bot]
774cfd8b65 Bump fastapi[standard] from 0.115.6 to 0.115.7 (#411)
Some checks are pending
docker-build / platform-excludes (push) Waiting to run
docker-build / build (push) Blocked by required conditions
docker-build / merge (push) Blocked by required conditions
pre-commit / pre-commit (push) Waiting to run
Run Pytest on Pull Request / test (push) Waiting to run
Bumps [fastapi[standard]](https://github.com/fastapi/fastapi) from 0.115.6 to 0.115.7.
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.115.6...0.115.7)

---
updated-dependencies:
- dependency-name: fastapi[standard]
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-25 19:35:42 +01:00
dependabot[bot]
9170b5f5cd Bump pydantic from 2.10.5 to 2.10.6 (#412)
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.10.5 to 2.10.6.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.10.5...v2.10.6)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-25 19:32:34 +01:00
dependabot[bot]
e42dd5d4b2 Bump timezonefinder from 6.5.7 to 6.5.8 (#414)
Bumps [timezonefinder](https://github.com/jannikmi/timezonefinder) from 6.5.7 to 6.5.8.
- [Release notes](https://github.com/jannikmi/timezonefinder/releases)
- [Changelog](https://github.com/jannikmi/timezonefinder/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jannikmi/timezonefinder/compare/6.5.7...6.5.8)

---
updated-dependencies:
- dependency-name: timezonefinder
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-25 19:29:00 +01:00
dependabot[bot]
58f077b4ae Bump numpydantic from 1.6.4 to 1.6.7 (#413)
Bumps [numpydantic](https://github.com/p2p-ld/numpydantic) from 1.6.4 to 1.6.7.
- [Release notes](https://github.com/p2p-ld/numpydantic/releases)
- [Changelog](https://github.com/p2p-ld/numpydantic/blob/main/docs/changelog.md)
- [Commits](https://github.com/p2p-ld/numpydantic/compare/v1.6.4...v1.6.7)

---
updated-dependencies:
- dependency-name: numpydantic
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-25 19:24:44 +01:00
115 changed files with 12466 additions and 5533 deletions

View File

@@ -195,6 +195,7 @@ jobs:
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest,enable={{is_default_branch}}
labels: |
org.opencontainers.image.licenses=${{ env.EOS_LICENSE }}
annotations: |

35
.github/workflows/stale.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: "Close stale pull requests/issues"
on:
schedule:
- cron: "16 00 * * *"
permissions:
contents: read
jobs:
stale:
name: Find Stale issues and PRs
runs-on: ubuntu-22.04
if: github.repository == 'Akkudoktor-EOS/EOS'
permissions:
pull-requests: write # to comment on stale pull requests
issues: write # to comment on stale issues
steps:
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
with:
stale-pr-message: 'This pull request has been marked as stale because it has been open (more
than) 90 days with no activity. Remove the stale label or add a comment saying that you
would like to have the label removed otherwise this pull request will automatically be
closed in 30 days. Note, that you can always re-open a closed pull request at any time.'
stale-issue-message: 'This issue has been marked as stale because it has been open (more
than) 90 days with no activity. Remove the stale label or add a comment saying that you
would like to have the label removed otherwise this issue will automatically be closed in
30 days. Note, that you can always re-open a closed issue at any time.'
days-before-stale: 90
days-before-close: 30
stale-issue-label: 'stale'
stale-pr-label: 'stale'
exempt-pr-labels: 'in progress'
exempt-issue-labels: 'feature request, enhancement'
operations-per-run: 400

2
.gitignore vendored
View File

@@ -179,7 +179,7 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.idea/
# General
.DS_Store

35
.gitlint Normal file
View File

@@ -0,0 +1,35 @@
[general]
# verbosity should be a value between 1 and 3, the commandline -v flags take precedence over this
verbosity = 3
regex-style-search=true
# Ignore rules, reference them by id or name (comma-separated)
ignore=title-trailing-punctuation, T3
# Enable specific community contributed rules
contrib=contrib-title-conventional-commits,CC1
# Set the extra-path where gitlint will search for user defined rules
extra-path=scripts/gitlint
[title-max-length]
line-length=80
[title-min-length]
min-length=5
[ignore-by-title]
# Match commit titles starting with "Release"
regex=^Release(.*)
ignore=title-max-length,body-min-length
[ignore-by-body]
# Match commits message bodies that have a line that contains 'release'
regex=(.*)release(.*)
ignore=all
[ignore-by-author-name]
# Match commits by author name (e.g. ignore dependabot commits)
regex=dependabot
ignore=all

View File

@@ -34,7 +34,7 @@ repos:
- "numpy==2.1.3"
pass_filenames: false
- repo: https://github.com/jackdewinter/pymarkdown
rev: main
rev: v0.9.29
hooks:
- id: pymarkdown
files: ^docs/
@@ -42,3 +42,7 @@ repos:
args:
- --config=docs/pymarkdown.json
- scan
- repo: https://github.com/jorisroovers/gitlint
rev: v0.19.1
hooks:
- id: gitlint

View File

@@ -6,7 +6,7 @@ The `EOS` project is in early development, therefore we encourage contribution i
## Documentation
Latest development documentation can be found at [Akkudoktor-EOS](https://akkudoktor-eos.readthedocs.io/en/main/).
Latest development documentation can be found at [Akkudoktor-EOS](https://akkudoktor-eos.readthedocs.io/en/latest/).
## Bug Reports
@@ -21,8 +21,8 @@ There are just too many possibilities and the project would drown in tickets oth
## Code Contributions
We welcome code contributions and bug fixes via [Pull Requests](https://github.com/Akkudoktor-EOS/EOS/pulls).
To make collaboration easier, we require pull requests to pass code style and unit tests.
To make collaboration easier, we require pull requests to pass code style, unit tests, and commit
message style checks.
### Setup development environment
@@ -60,6 +60,7 @@ To run formatting automatically before every commit:
```bash
pre-commit install
pre-commit install --hook-type commit-msg
```
Or run them manually:
@@ -75,3 +76,8 @@ Use `pytest` to run tests locally:
```bash
python -m pytest -vs --cov src --cov-report term-missing tests/
```
### Commit message style
Our commit message checks use [`gitlint`](https://github.com/jorisroovers/gitlint). The checks
enforce the [`Conventional Commits`](https://www.conventionalcommits.org) commit message style.

View File

@@ -1,3 +1,4 @@
# syntax=docker/dockerfile:1.7
ARG PYTHON_VERSION=3.12.7
FROM python:${PYTHON_VERSION}-slim
@@ -9,6 +10,9 @@ ENV EOS_CACHE_DIR="${EOS_DIR}/cache"
ENV EOS_OUTPUT_DIR="${EOS_DIR}/output"
ENV EOS_CONFIG_DIR="${EOS_DIR}/config"
# Overwrite when starting the container in a production environment
ENV EOS_SERVER__EOSDASH_SESSKEY=s3cr3t
WORKDIR ${EOS_DIR}
RUN adduser --system --group --no-create-home eos \
@@ -31,6 +35,19 @@ RUN mkdir -p src && pip install -e .
COPY src src
# Create minimal default configuration for Docker to fix EOSDash accessibility (#629)
# This ensures EOSDash binds to 0.0.0.0 instead of 127.0.0.1 in containers
RUN echo '{\n\
"server": {\n\
"host": "0.0.0.0",\n\
"port": 8503,\n\
"startup_eosdash": true,\n\
"eosdash_host": "0.0.0.0",\n\
"eosdash_port": 8504\n\
}\n\
}' > "${EOS_CONFIG_DIR}/EOS.config.json" \
&& chown eos:eos "${EOS_CONFIG_DIR}/EOS.config.json"
USER eos
ENTRYPOINT []

View File

@@ -1,5 +1,5 @@
# Define the targets
.PHONY: help venv pip install dist test test-full docker-run docker-build docs read-docs clean format mypy run run-dev
.PHONY: help venv pip install dist test test-full docker-run docker-build docs read-docs clean format gitlint mypy run run-dev
# Default target
all: help
@@ -11,18 +11,22 @@ help:
@echo " pip - Install dependencies from requirements.txt."
@echo " pip-dev - Install dependencies from requirements-dev.txt."
@echo " format - Format source code."
@echo " gitlint - Lint last commit message."
@echo " mypy - Run mypy."
@echo " install - Install EOS in editable form (development mode) into virtual environment."
@echo " docker-run - Run entire setup on docker"
@echo " docker-build - Rebuild docker image"
@echo " docs - Generate HTML documentation (in build/docs/html/)."
@echo " read-docs - Read HTML documentation in your browser."
@echo " gen-docs - Generate openapi.json and docs/_generated/*.""
@echo " clean-docs - Remove generated documentation.""
@echo " gen-docs - Generate openapi.json and docs/_generated/*."
@echo " clean-docs - Remove generated documentation."
@echo " run - Run EOS production server in virtual environment."
@echo " run-dev - Run EOS development server in virtual environment (automatically reloads)."
@echo " run-dash - Run EOSdash production server in virtual environment."
@echo " run-dash-dev - Run EOSdash development server in virtual environment (automatically reloads)."
@echo " test - Run tests."
@echo " test-full - Run tests with full optimization."
@echo " test-ci - Run tests as CI does. No user config file allowed."
@echo " dist - Create distribution (in dist/)."
@echo " clean - Remove generated documentation, distribution and virtual environment."
@@ -72,6 +76,11 @@ read-docs: docs
@echo "Read the documentation in your browser"
.venv/bin/python -m webbrowser build/docs/html/index.html
# Clean Python bytecode
clean-bytecode:
find . -type d -name "__pycache__" -exec rm -r {} +
find . -type f -name "*.pyc" -delete
# Clean target to remove generated documentation and documentation artefacts
clean-docs:
@echo "Searching and deleting all '_autosum' directories in docs..."
@@ -110,6 +119,11 @@ test:
@echo "Running tests..."
.venv/bin/pytest -vs --cov src --cov-report term-missing
# Target to run tests as done by CI on Github.
test-ci:
@echo "Running tests as CI..."
.venv/bin/pytest --full-run --check-config-side-effect -vs --cov src --cov-report term-missing
# Target to run all tests.
test-full:
@echo "Running all tests..."
@@ -119,6 +133,10 @@ test-full:
format:
.venv/bin/pre-commit run --all-files
# Target to trigger gitlint using pre-commit for the last commit message
gitlint:
.venv/bin/pre-commit run gitlint --hook-stage commit-msg --commit-msg-filename .git/COMMIT_EDITMSG
# Target to format code.
mypy:
.venv/bin/mypy

View File

@@ -8,9 +8,19 @@ Documentation can be found at [Akkudoktor-EOS](https://akkudoktor-eos.readthedoc
See [CONTRIBUTING.md](CONTRIBUTING.md).
## System requirements
- Python >= 3.11, < 3.13
- Architecture: amd64, aarch64 (armv8)
- OS: Linux, Windows, macOS
Note: For Python 3.13 some dependencies (e.g. [Pendulum](https://github.com/python-pendulum/Pendulum)) are not yet available on https://pypi.org and have to be manually compiled (a recent [Rust](https://www.rust-lang.org/tools/install) installation is required).
Other architectures (e.g. armv6, armv7) are unsupported for now, because a multitude of dependencies are not available on https://piwheels.org and have to be built manually (a recent Rust installation and [GCC](https://gcc.gnu.org/) are required, Python 3.11 is recommended).
## Installation
The project requires Python 3.11 or newer. Official docker images can be found at [akkudoktor/eos](https://hub.docker.com/r/akkudoktor/eos).
The project requires Python 3.11 or newer. Docker images (amd64/aarch64) can be found at [akkudoktor/eos](https://hub.docker.com/r/akkudoktor/eos).
Following sections describe how to locally start the EOS server on `http://localhost:8503`.
@@ -30,11 +40,12 @@ Windows:
```cmd
python -m venv .venv
.venv\Scripts\Activate
.venv\Scripts\pip install -r requirements.txt
.venv\Scripts\pip install -e .
```
Finally, start the EOS server:
Finally, start the EOS server to access it at `http://localhost:8503` (API docs at `http://localhost:8503/docs`):
Linux:
@@ -50,6 +61,8 @@ Windows:
### Docker
Start EOS with following command to access it at `http://localhost:8503` (API docs at `http://localhost:8503/docs`):
```bash
docker compose up
```

View File

@@ -21,5 +21,25 @@ services:
- EOS_ELECPRICE__PROVIDER=ElecPriceAkkudoktor
- EOS_ELECPRICE__CHARGES_KWH=0.21
ports:
- "${EOS_SERVER__PORT}:${EOS_SERVER__PORT}"
- "${EOS_SERVER__EOSDASH_PORT}:${EOS_SERVER__EOSDASH_PORT}"
# Configure what ports to expose on host
- "${EOS_SERVER__PORT}:8503"
- "${EOS_SERVER__EOSDASH_PORT}:8504"
# Volume mount configuration (optional)
# IMPORTANT: When mounting local directories, the default config won't be available.
# You must create an EOS.config.json file in your local config directory with:
# {
# "server": {
# "host": "0.0.0.0", # Required for Docker container accessibility
# "port": 8503,
# "startup_eosdash": true,
# "eosdash_host": "0.0.0.0", # Required for Docker container accessibility
# "eosdash_port": 8504
# }
# }
#
# Example volume mounts (uncomment to use):
# volumes:
# - ./config:/opt/eos/config # Mount local config directory
# - ./cache:/opt/eos/cache # Mount local cache directory
# - ./output:/opt/eos/output # Mount local output directory

View File

@@ -15,10 +15,6 @@ Properties:
timezone (Optional[str]): Computed time zone string based on the specified latitude
and longitude.
Validators:
validate_latitude (float): Ensures `latitude` is within the range -90 to 90.
validate_longitude (float): Ensures `longitude` is within the range -180 to 180.
:::{table} general
:widths: 10 20 10 5 5 30
:align: left
@@ -127,8 +123,10 @@ Validators:
| Name | Environment Variable | Type | Read-Only | Default | Description |
| ---- | -------------------- | ---- | --------- | ------- | ----------- |
| level | `EOS_LOGGING__LEVEL` | `Optional[str]` | `rw` | `None` | EOS default logging level. |
| root_level | | `str` | `ro` | `N/A` | Root logger logging level. |
| level | `EOS_LOGGING__LEVEL` | `Optional[str]` | `rw` | `None` | This is deprecated. Use console_level and file_level instead. |
| console_level | `EOS_LOGGING__CONSOLE_LEVEL` | `Optional[str]` | `rw` | `None` | Logging level when logging to console. |
| file_level | `EOS_LOGGING__FILE_LEVEL` | `Optional[str]` | `rw` | `None` | Logging level when logging to file. |
| file_path | | `Optional[pathlib.Path]` | `ro` | `N/A` | Computed log file path based on data output path. |
:::
### Example Input
@@ -138,7 +136,9 @@ Validators:
{
"logging": {
"level": "INFO"
"level": null,
"console_level": "TRACE",
"file_level": "TRACE"
}
}
```
@@ -150,8 +150,10 @@ Validators:
{
"logging": {
"level": "INFO",
"root_level": "INFO"
"level": null,
"console_level": "TRACE",
"file_level": "TRACE",
"file_path": "/home/user/.local/share/net.akkudoktoreos.net/output/eos.log"
}
}
```
@@ -422,6 +424,7 @@ Validators:
| ---- | -------------------- | ---- | --------- | ------- | ----------- |
| 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). |
| vat_rate | `EOS_ELECPRICE__VAT_RATE` | `Optional[float]` | `rw` | `1.19` | VAT rate factor applied to electricity price when charges are used. |
| provider_settings | `EOS_ELECPRICE__PROVIDER_SETTINGS` | `Optional[akkudoktoreos.prediction.elecpriceimport.ElecPriceImportCommonSettings]` | `rw` | `None` | Provider settings |
:::
@@ -434,6 +437,7 @@ Validators:
"elecprice": {
"provider": "ElecPriceAkkudoktor",
"charges_kwh": 0.21,
"vat_rate": 1.19,
"provider_settings": null
}
}
@@ -475,7 +479,7 @@ Validators:
| Name | Environment Variable | Type | Read-Only | Default | Description |
| ---- | -------------------- | ---- | --------- | ------- | ----------- |
| 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 |
| provider_settings | `EOS_LOAD__PROVIDER_SETTINGS` | `Union[akkudoktoreos.prediction.loadakkudoktor.LoadAkkudoktorCommonSettings, akkudoktoreos.prediction.loadvrm.LoadVrmCommonSettings, akkudoktoreos.prediction.loadimport.LoadImportCommonSettings, NoneType]` | `rw` | `None` | Provider settings |
:::
### Example Input/Output
@@ -518,6 +522,33 @@ Validators:
}
```
### Common settings for VRM API
:::{table} load::provider_settings
:widths: 10 10 5 5 30
:align: left
| Name | Type | Read-Only | Default | Description |
| ---- | ---- | --------- | ------- | ----------- |
| load_vrm_token | `str` | `rw` | `your-token` | Token for Connecting VRM API |
| load_vrm_idsite | `int` | `rw` | `12345` | VRM-Installation-ID |
:::
#### Example Input/Output
```{eval-rst}
.. code-block:: json
{
"load": {
"provider_settings": {
"load_vrm_token": "your-token",
"load_vrm_idsite": 12345
}
}
}
```
### Common settings for load data import from file
:::{table} load::provider_settings
@@ -552,8 +583,9 @@ Validators:
| Name | Environment Variable | Type | Read-Only | Default | Description |
| ---- | -------------------- | ---- | --------- | ------- | ----------- |
| provider | `EOS_PVFORECAST__PROVIDER` | `Optional[str]` | `rw` | `None` | PVForecast provider id of provider to be used. |
| provider_settings | `EOS_PVFORECAST__PROVIDER_SETTINGS` | `Union[akkudoktoreos.prediction.pvforecastimport.PVForecastImportCommonSettings, akkudoktoreos.prediction.pvforecastvrm.PVforecastVrmCommonSettings, NoneType]` | `rw` | `None` | Provider settings |
| planes | `EOS_PVFORECAST__PLANES` | `Optional[list[akkudoktoreos.prediction.pvforecast.PVForecastPlaneSetting]]` | `rw` | `None` | Plane configuration. |
| provider_settings | `EOS_PVFORECAST__PROVIDER_SETTINGS` | `Optional[akkudoktoreos.prediction.pvforecastimport.PVForecastImportCommonSettings]` | `rw` | `None` | Provider settings |
| max_planes | `EOS_PVFORECAST__MAX_PLANES` | `Optional[int]` | `rw` | `0` | Maximum number of planes that can be set |
| planes_peakpower | | `List[float]` | `ro` | `N/A` | Compute a list of the peak power per active planes. |
| planes_azimuth | | `List[float]` | `ro` | `N/A` | Compute a list of the azimuths per active planes. |
| planes_tilt | | `List[float]` | `ro` | `N/A` | Compute a list of the tilts per active planes. |
@@ -569,10 +601,11 @@ Validators:
{
"pvforecast": {
"provider": "PVForecastAkkudoktor",
"provider_settings": null,
"planes": [
{
"surface_tilt": 10.0,
"surface_azimuth": 10.0,
"surface_azimuth": 180.0,
"userhorizon": [
10.0,
20.0,
@@ -594,7 +627,7 @@ Validators:
},
{
"surface_tilt": 20.0,
"surface_azimuth": 20.0,
"surface_azimuth": 90.0,
"userhorizon": [
5.0,
15.0,
@@ -615,7 +648,7 @@ Validators:
"strings_per_inverter": 2
}
],
"provider_settings": null
"max_planes": 0
}
}
```
@@ -628,10 +661,11 @@ Validators:
{
"pvforecast": {
"provider": "PVForecastAkkudoktor",
"provider_settings": null,
"planes": [
{
"surface_tilt": 10.0,
"surface_azimuth": 10.0,
"surface_azimuth": 180.0,
"userhorizon": [
10.0,
20.0,
@@ -653,7 +687,7 @@ Validators:
},
{
"surface_tilt": 20.0,
"surface_azimuth": 20.0,
"surface_azimuth": 90.0,
"userhorizon": [
5.0,
15.0,
@@ -674,14 +708,14 @@ Validators:
"strings_per_inverter": 2
}
],
"provider_settings": null,
"max_planes": 0,
"planes_peakpower": [
5.0,
3.5
],
"planes_azimuth": [
10.0,
20.0
180.0,
90.0
],
"planes_tilt": [
10.0,
@@ -707,6 +741,116 @@ Validators:
}
```
### PV Forecast Plane Configuration
:::{table} pvforecast::planes::list
:widths: 10 10 5 5 30
:align: left
| Name | Type | Read-Only | Default | Description |
| ---- | ---- | --------- | ------- | ----------- |
| surface_tilt | `Optional[float]` | `rw` | `30.0` | Tilt angle from horizontal plane. Ignored for two-axis tracking. |
| surface_azimuth | `Optional[float]` | `rw` | `180.0` | Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270). |
| userhorizon | `Optional[List[float]]` | `rw` | `None` | Elevation of horizon in degrees, at equally spaced azimuth clockwise from north. |
| peakpower | `Optional[float]` | `rw` | `None` | Nominal power of PV system in kW. |
| pvtechchoice | `Optional[str]` | `rw` | `crystSi` | PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'. |
| mountingplace | `Optional[str]` | `rw` | `free` | Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated. |
| loss | `Optional[float]` | `rw` | `14.0` | Sum of PV system losses in percent |
| trackingtype | `Optional[int]` | `rw` | `None` | Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south. |
| optimal_surface_tilt | `Optional[bool]` | `rw` | `False` | Calculate the optimum tilt angle. Ignored for two-axis tracking. |
| optimalangles | `Optional[bool]` | `rw` | `False` | Calculate the optimum tilt and azimuth angles. Ignored for two-axis tracking. |
| albedo | `Optional[float]` | `rw` | `None` | Proportion of the light hitting the ground that it reflects back. |
| module_model | `Optional[str]` | `rw` | `None` | Model of the PV modules of this plane. |
| inverter_model | `Optional[str]` | `rw` | `None` | Model of the inverter of this plane. |
| inverter_paco | `Optional[int]` | `rw` | `None` | AC power rating of the inverter [W]. |
| modules_per_string | `Optional[int]` | `rw` | `None` | Number of the PV modules of the strings of this plane. |
| strings_per_inverter | `Optional[int]` | `rw` | `None` | Number of the strings of the inverter of this plane. |
:::
#### Example Input/Output
```{eval-rst}
.. code-block:: json
{
"pvforecast": {
"planes": [
{
"surface_tilt": 10.0,
"surface_azimuth": 180.0,
"userhorizon": [
10.0,
20.0,
30.0
],
"peakpower": 5.0,
"pvtechchoice": "crystSi",
"mountingplace": "free",
"loss": 14.0,
"trackingtype": 0,
"optimal_surface_tilt": false,
"optimalangles": false,
"albedo": null,
"module_model": null,
"inverter_model": null,
"inverter_paco": 6000,
"modules_per_string": 20,
"strings_per_inverter": 2
},
{
"surface_tilt": 20.0,
"surface_azimuth": 90.0,
"userhorizon": [
5.0,
15.0,
25.0
],
"peakpower": 3.5,
"pvtechchoice": "crystSi",
"mountingplace": "free",
"loss": 14.0,
"trackingtype": 1,
"optimal_surface_tilt": false,
"optimalangles": false,
"albedo": null,
"module_model": null,
"inverter_model": null,
"inverter_paco": 4000,
"modules_per_string": 20,
"strings_per_inverter": 2
}
]
}
}
```
### Common settings for VRM API
:::{table} pvforecast::provider_settings
:widths: 10 10 5 5 30
:align: left
| Name | Type | Read-Only | Default | Description |
| ---- | ---- | --------- | ------- | ----------- |
| pvforecast_vrm_token | `str` | `rw` | `your-token` | Token for Connecting VRM API |
| pvforecast_vrm_idsite | `int` | `rw` | `12345` | VRM-Installation-ID |
:::
#### Example Input/Output
```{eval-rst}
.. code-block:: json
{
"pvforecast": {
"provider_settings": {
"pvforecast_vrm_token": "your-token",
"pvforecast_vrm_idsite": 12345
}
}
}
```
### Common settings for pvforecast data import from file or JSON string
:::{table} pvforecast::provider_settings
@@ -734,89 +878,6 @@ Validators:
}
```
### PV Forecast Plane Configuration
:::{table} pvforecast::planes::list
:widths: 10 10 5 5 30
:align: left
| Name | Type | Read-Only | Default | Description |
| ---- | ---- | --------- | ------- | ----------- |
| surface_tilt | `Optional[float]` | `rw` | `None` | Tilt angle from horizontal plane. Ignored for two-axis tracking. |
| surface_azimuth | `Optional[float]` | `rw` | `None` | Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270). |
| userhorizon | `Optional[List[float]]` | `rw` | `None` | Elevation of horizon in degrees, at equally spaced azimuth clockwise from north. |
| peakpower | `Optional[float]` | `rw` | `None` | Nominal power of PV system in kW. |
| pvtechchoice | `Optional[str]` | `rw` | `crystSi` | PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'. |
| mountingplace | `Optional[str]` | `rw` | `free` | Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated. |
| loss | `Optional[float]` | `rw` | `14.0` | Sum of PV system losses in percent |
| trackingtype | `Optional[int]` | `rw` | `None` | Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south. |
| optimal_surface_tilt | `Optional[bool]` | `rw` | `False` | Calculate the optimum tilt angle. Ignored for two-axis tracking. |
| optimalangles | `Optional[bool]` | `rw` | `False` | Calculate the optimum tilt and azimuth angles. Ignored for two-axis tracking. |
| albedo | `Optional[float]` | `rw` | `None` | Proportion of the light hitting the ground that it reflects back. |
| module_model | `Optional[str]` | `rw` | `None` | Model of the PV modules of this plane. |
| inverter_model | `Optional[str]` | `rw` | `None` | Model of the inverter of this plane. |
| inverter_paco | `Optional[int]` | `rw` | `None` | AC power rating of the inverter. [W] |
| modules_per_string | `Optional[int]` | `rw` | `None` | Number of the PV modules of the strings of this plane. |
| strings_per_inverter | `Optional[int]` | `rw` | `None` | Number of the strings of the inverter of this plane. |
:::
#### Example Input/Output
```{eval-rst}
.. code-block:: json
{
"pvforecast": {
"planes": [
{
"surface_tilt": 10.0,
"surface_azimuth": 10.0,
"userhorizon": [
10.0,
20.0,
30.0
],
"peakpower": 5.0,
"pvtechchoice": "crystSi",
"mountingplace": "free",
"loss": 14.0,
"trackingtype": 0,
"optimal_surface_tilt": false,
"optimalangles": false,
"albedo": null,
"module_model": null,
"inverter_model": null,
"inverter_paco": 6000,
"modules_per_string": 20,
"strings_per_inverter": 2
},
{
"surface_tilt": 20.0,
"surface_azimuth": 20.0,
"userhorizon": [
5.0,
15.0,
25.0
],
"peakpower": 3.5,
"pvtechchoice": "crystSi",
"mountingplace": "free",
"loss": 14.0,
"trackingtype": 1,
"optimal_surface_tilt": false,
"optimalangles": false,
"albedo": null,
"module_model": null,
"inverter_model": null,
"inverter_paco": 4000,
"modules_per_string": 20,
"strings_per_inverter": 2
}
]
}
}
```
## Weather Forecast Configuration
:::{table} weather
@@ -877,11 +938,11 @@ Validators:
| Name | Environment Variable | Type | Read-Only | Default | Description |
| ---- | -------------------- | ---- | --------- | ------- | ----------- |
| host | `EOS_SERVER__HOST` | `Optional[pydantic.networks.IPvAnyAddress]` | `rw` | `0.0.0.0` | EOS server IP address. |
| host | `EOS_SERVER__HOST` | `Optional[pydantic.networks.IPvAnyAddress]` | `rw` | `127.0.0.1` | 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_host | `EOS_SERVER__EOSDASH_HOST` | `Optional[pydantic.networks.IPvAnyAddress]` | `rw` | `127.0.0.1` | EOSdash server IP address. |
| eosdash_port | `EOS_SERVER__EOSDASH_PORT` | `Optional[int]` | `rw` | `8504` | EOSdash server IP port number. |
:::
@@ -892,11 +953,11 @@ Validators:
{
"server": {
"host": "0.0.0.0",
"host": "127.0.0.1",
"port": 8503,
"verbose": false,
"startup_eosdash": true,
"eosdash_host": "0.0.0.0",
"eosdash_host": "127.0.0.1",
"eosdash_port": 8504
}
}
@@ -943,7 +1004,9 @@ Validators:
"interval": 300.0
},
"logging": {
"level": "INFO"
"level": null,
"console_level": "TRACE",
"file_level": "TRACE"
},
"devices": {
"batteries": [
@@ -989,6 +1052,7 @@ Validators:
"elecprice": {
"provider": "ElecPriceAkkudoktor",
"charges_kwh": 0.21,
"vat_rate": 1.19,
"provider_settings": null
},
"load": {
@@ -997,10 +1061,11 @@ Validators:
},
"pvforecast": {
"provider": "PVForecastAkkudoktor",
"provider_settings": null,
"planes": [
{
"surface_tilt": 10.0,
"surface_azimuth": 10.0,
"surface_azimuth": 180.0,
"userhorizon": [
10.0,
20.0,
@@ -1022,7 +1087,7 @@ Validators:
},
{
"surface_tilt": 20.0,
"surface_azimuth": 20.0,
"surface_azimuth": 90.0,
"userhorizon": [
5.0,
15.0,
@@ -1043,18 +1108,18 @@ Validators:
"strings_per_inverter": 2
}
],
"provider_settings": null
"max_planes": 0
},
"weather": {
"provider": "WeatherImport",
"provider_settings": null
},
"server": {
"host": "0.0.0.0",
"host": "127.0.0.1",
"port": 8503,
"verbose": false,
"startup_eosdash": true,
"eosdash_host": "0.0.0.0",
"eosdash_host": "127.0.0.1",
"eosdash_port": 8504
},
"utils": {}

View File

@@ -430,7 +430,13 @@ Returns:
**Request Body**:
- `application/json`: {
"description": "The value to assign to the specified configuration path.",
"anyOf": [
{},
{
"type": "null"
}
],
"description": "The value to assign to the specified configuration path (can be None).",
"title": "Value"
}
@@ -458,6 +464,55 @@ Health check endpoint to verify that the EOS server is alive.
---
## GET /v1/logging/log
**Links**: [local](http://localhost:8503/docs#/default/fastapi_logging_get_log_v1_logging_log_get), [eos](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_logging_get_log_v1_logging_log_get)
Fastapi Logging Get Log
```
Get structured log entries from the EOS log file.
Filters and returns log entries based on the specified query parameters. The log
file is expected to contain newline-delimited JSON entries.
Args:
limit (int): Maximum number of entries to return.
level (Optional[str]): Filter logs by severity level (e.g., DEBUG, INFO).
contains (Optional[str]): Return only logs that include this string in the message.
regex (Optional[str]): Return logs that match this regular expression in the message.
from_time (Optional[str]): ISO 8601 timestamp to filter logs not older than this.
to_time (Optional[str]): ISO 8601 timestamp to filter logs not newer than this.
tail (bool): If True, fetch the most recent log entries (like `tail`).
Returns:
JSONResponse: A JSON list of log entries.
```
**Parameters**:
- `limit` (query, optional): Maximum number of log entries to return.
- `level` (query, optional): Filter by log level (e.g., INFO, ERROR).
- `contains` (query, optional): Filter logs containing this substring.
- `regex` (query, optional): Filter logs by matching regex in message.
- `from_time` (query, optional): Start time (ISO format) for filtering logs.
- `to_time` (query, optional): End time (ISO format) for filtering logs.
- `tail` (query, optional): If True, returns the most recent lines (tail mode).
**Responses**:
- **200**: Successful Response
- **422**: Validation Error
---
## PUT /v1/measurement/data
**Links**: [local](http://localhost:8503/docs#/default/fastapi_measurement_data_put_v1_measurement_data_put), [eos](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_measurement_data_put_v1_measurement_data_put)
@@ -743,7 +798,8 @@ Args:
"$ref": "#/components/schemas/PydanticDateTimeData"
},
{
"type": "object"
"type": "object",
"additionalProperties": true
},
{
"type": "null"

BIN
docs/_static/introduction/integration.png vendored Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
docs/_static/introduction/introduction.png vendored Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
docs/_static/introduction/overview.png vendored Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

@@ -1,9 +0,0 @@
% SPDX-License-Identifier: Apache-2.0
# About Akkudoktor EOS
The Energy System Simulation and Optimization System (EOS) 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

@@ -28,7 +28,7 @@ management.
Energy management is the overall process to provide planning data for scheduling the different
devices in your system in an optimal way. Energy management cares for the update of predictions and
the optimization of the planning based on the simulated behavior of the devices. The planning is on
the hour. Sub-hour energy management is left
the hour.
### Optimization

View File

@@ -1,4 +1,5 @@
% SPDX-License-Identifier: Apache-2.0
(integration-page)=
# Integration
@@ -19,15 +20,22 @@ Andreas Schmitz uses [Node-RED](https://nodered.org/) as part of his home automa
### Node-Red Resources
- [Installation Guide (German)](https://meintechblog.de/2024/09/05/andreas-schmitz-joerg-installiert-mein-energieoptimierungssystem/)
\— A detailed guide on integrating an early version of EOS with `Node-RED`.
- [Installation Guide (German)](https://www.youtube.com/playlist?list=PL8_vk9A-s7zLD865Oou6y3EeQLlNtu-Hn)
\— A detailed guide on integrating EOS with `Node-RED`.
## Home Assistant
[Home Assistant](https://www.home-assistant.io/) is an open-source home automation platform that
emphasizes local control and user privacy.
(duetting-solution)=
### Home Assistant Resources
- Duetting's [EOS Home Assistant Addon](https://github.com/Duetting/ha_eos_addon) — Additional
details can be found in this [discussion thread](https://github.com/Akkudoktor-EOS/EOS/discussions/294).
## EOS Connect
[EOS connect](https://github.com/ohAnd/EOS_connect) uses `EOS` for energy management and optimization,
and connects to smart home platforms to monitor, forecast, and control energy flows.

View File

@@ -0,0 +1,180 @@
% SPDX-License-Identifier: Apache-2.0
# Introduction
The Energy System Simulation and Optimization System (EOS) 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.
After successfully installing a PV system with or without battery storage, most owners
first priority is often to charge the electric car with surplus energy in order to use
the electricity generated by the PV system cost-effectively for electromobility.
After initial experiences, the desire to include battery storage and dynamic electricity
prices in the solution soon arises. The market already offers various commercial and
non-commercial solutions for this, such as the popular open source hardware and software
solutions evcc or openWB.
Some solutions take into account the current values of the system such as PV power
output, battery storage charge level or the current electricity price to decide whether
to charge the electric car with PV surplus or from the grid (e.g. openWB), some use
historical consumption values and PV forecast data for their calculations, but leave out
the current electricity prices and charging the battery storage from the power grid
(Predbat). Others are specialiced on working in combination with a specific smart home
solution (e.g. emhass). Still others focus on certain consumers, such as the electric car,
or are currently working on integrating the forecast values (evcc). And some are commercial
devices that require an electrician to install them and expect a certain ecosystem
(e.g. Sunny Home Manager).
The Akkudoktor EOS
- takes into account historical, current and forecast data such as consumption values, PV
forecast data, electricity price forecast, battery storage and electric car charge levels
- the simulation also takes into account the possibility of charging the battery storage
from the grid at low electricity prices
- is not limited to certain consumers, but includes electric cars, heat pumps or more
powerful consumers such as tumble dryers
- is independent of a specific smart home solution and can also be integrated into
self-developed solutions if desired
- is a free and independent open source software solution
![Introdution](../_static/introduction/introduction.png)
The challenge is to charge (electric car) or start the consumers (washing machine, dryer)
at the right time and to do so as cost-efficiently as possible. If PV yield forecast,
battery storage and dynamic electricity price forecasts are included in the calculation,
the possibilities increase, but unfortunately so does the complexity.
The Akkudoktor EOS addresses this challenge by simulating energy flows in the household
based on target values, forecast data and current operating data over a 48-hour
observation period, running through a large number of different scenarios and finally
providing a cost-optimized plan for the current day controlling the relevant consumers.
## Prerequisites
- Technical requirements
- Input data
### Technical requirements
- reasonably fast computer on which EOS is installed
- controllable energy system consisting of photovoltaic system, solar battery storage,
energy intensive consumers that must provide the appropriate interfaces
- integration solution for integrating the energy system and EOS
### Input Data
![Overview](../_static/introduction/overview.png)
The EOS requires various types of data for the simulation:
Forecast data
- PV yield forecast
- Expected household consumption
- Electricity price forecast
- Forecast temperature trend (if heatpump is used)
Basic data and current operating data
- Current charge level of the battery storage
- Value of electricity in the battery storage
- Current charge level of the electric car
- Energy consumption and running time of dishwasher, washing machine and tumble dryer
Target values
- Charge level the electric car should reach in the next few hours
- Consumers to run in the next few hours
There are various service providers available for PV forecasting that calculate forecast
data for a PV system based on the various influencing factors, such as system size,
orientation, location, time of year and weather conditions. EOS also offers a
[PV forecasting service](#prediction-page) which can be used. This service uses
public data in the background.
For the forecast of household consumption EOS provides a standard load curve for an
average day based on annual household consumption that you can fetch via API. This data
was compiled based on data from several households and provides an initial usable basis.
Alternatively your own collected historical data could be used to reflect your personal
consumption behaviour.
## Simulation Results
Based on the input data, the EOS uses a genetic algorithm to create a cost-optimized
schedule for the coming hours from numerous simulations of the overall system.
The plan created contains for each of the coming hours
- Control information
- whether and with what power the battery storage should be charged from the grid
- when the battery storage should be charged via the PV system
- whether discharging the battery storage is permitted or not
- when and with what power the electric car should be charged
- when a household appliance should be activated
- Energy history information
- Total load of the house
- Grid consumption
- Feed-in
- Load of the planned household appliances
- Charge level of the battery storage
- Charge level of the electric car
- Active losses
- Cost information
- Revenue per hour (when fed into the grid)
- Total costs per hour (when drawn from the grid)
- Overall balance (revenue-costs)
- Cost development
If required, the simulation result can also be created and downloaded in graphical
form as a PDF from EOS.
## Integration
The Akkudoktor EOS can be integrated into a wide variety of systems with a variety
of components.
![Integration](../_static/introduction/integration.png)
However, the components are not integrated by the EOS itself, but must be integrated by
the user using an integration solution and currently requires some effort and technical
know-how.
Any [integration](#integration-page) solution that can act as an intermediary between the
components and the REST API of EOS can be used. One possible solution that enables the
integration of components and EOS is Node-RED. Another solution could be Home Assistant
usings its built in features.
Access to the data and functions of the components can be done in a variety of ways.
Node-RED offers a large number of types of nodes that allow access via the protocols
commonly used in this area, such as Modbus or MQTT. Access to any existing databases,
such as InfluxDB or PostgreSQL, is also possible via nodes provided by Node-RED.
It becomes easier if a smart home solution like Home Assistant, openHAB or ioBroker or
solutions such as evcc or openWB are already in use. In this case, these smart home
solutions already take over the technical integration and communication with the components
at a technical level and Node-RED offers nodes for accessing these solutions, so that the
corresponding sources can be easily integrated into a flow.
In Home Assistant you could use an automation to prepare the input payload for EOS and
then use the RESTful integration to call EOS. Based on this concept there is already a
Home Assistant add-on created by [Duetting](#duetting-solution).
The plan created by EOS must also be executed via the chosen integration solution,
with the respective devices receiving their instructions according to the plan.
## Limitations
The plan calculated by EOS is cost-optimized due to the genetic algorithm used, but not
necessarily cost-optimal, since genetic algorithms do not always find the global optimum,
but usually find good local optima very quickly in a large solution space.
## Links
- [German Videos explaining the basic concept and installation process of EOS (YouTube)](https://www.youtube.com/playlist?list=PL8_vk9A-s7zLD865Oou6y3EeQLlNtu-Hn)
- [German Forum of Akkudoktor EOS](https://akkudoktor.net/c/der-akkudoktor/eos)
- [Akkudoktor-EOS GitHub Repository](https://github.com/Akkudoktor-EOS/EOS)
- [Latest EOS Documentation](https://akkudoktor-eos.readthedocs.io/en/latest/)

View File

@@ -0,0 +1,75 @@
% SPDX-License-Identifier: Apache-2.0
(logging-page)=
# Logging
EOS automatically records important events and messages to help you understand whats happening and
to troubleshoot problems.
## How Logging Works
- By default, logs are shown in your terminal (console).
- You can also save logs to a file for later review.
- Log files are rotated automatically to avoid becoming too large.
## Controlling Log Details
### 1. Command-Line Option
Set the amount of log detail shown on the console by using `--log-level` when starting EOS.
Example:
```{eval-rst}
.. tabs::
.. tab:: Windows
.. code-block:: powershell
.venv\Scripts\python src/akkudoktoreos/server/eos.py --log-level DEBUG
.. tab:: Linux
.. code-block:: bash
.venv/bin/python src/akkudoktoreos/server/eos.py --log-level DEBUG
```
Common levels:
- DEBUG (most detail)
- INFO (default)
- WARNING
- ERROR
- CRITICAL (least detail)
### 2. Configuration File
You can also set logging options in your EOS configuration file (EOS.config.json).
```Json
{
"logging": {
"console_level": "INFO",
"file_level": "DEBUG"
}
}
```
### 3. Environment Variable
You can also control the log level by setting the `EOS_LOGGING__CONSOLE_LEVEL` and the
`EOS_LOGGING__FILE_LEVEL` environment variables.
```bash
EOS_LOGGING__CONSOLE_LEVEL="INFO"
EOS_LOGGING__FILE_LEVEL="DEBUG"
```
## File Logging
If the `file_level` configuration is set, log records are written to a rotating log file. The log
file is in the data output directory and named `eos.log`. You may directly read the file or use
the `/v1/logging/log` endpoint to access the file log.

View File

@@ -21,6 +21,7 @@ including electricity prices, battery storage capacity, PV forecast, and tempera
"strompreis_euro_pro_wh": [0.0003784, 0.0003868, ..., 0.00034102, 0.00033709]
},
"pv_akku": {
"device_id": "battery1",
"capacity_wh": 12000,
"charging_efficiency": 0.92,
"discharging_efficiency": 0.92,
@@ -30,9 +31,12 @@ including electricity prices, battery storage capacity, PV forecast, and tempera
"max_soc_percentage": 100
},
"inverter": {
"device_id": "inverter1",
"max_power_wh": 15500
"battery_id": "battery1",
},
"eauto": {
"device_id": "auto1",
"capacity_wh": 64000,
"charging_efficiency": 0.88,
"discharging_efficiency": 0.88,
@@ -95,6 +99,7 @@ Verify prices against your local tariffs.
#### Configuration
- `device_id`: ID of battery
- `capacity_wh`: Total battery capacity in Wh
- `charging_efficiency`: Charging efficiency (0-1)
- `discharging_efficiency`: Discharging efficiency (0-1)
@@ -108,10 +113,13 @@ Verify prices against your local tariffs.
### Inverter
- `device_id`: ID of inverter
- `max_power_wh`: Maximum inverter power in Wh
- `battery_id`: ID of battery
### Electric Vehicle (EV)
- `device_id`: ID of electric vehicle
- `capacity_wh`: Battery capacity in Wh
- `charging_efficiency`: Charging efficiency (0-1)
- `discharging_efficiency`: Discharging efficiency (0-1)

View File

@@ -1,4 +1,5 @@
% SPDX-License-Identifier: Apache-2.0
(prediction-page)=
# Predictions
@@ -118,9 +119,11 @@ Configuration options:
- `provider`: Electricity price provider id of provider to be used.
- `ElecPriceAkkudoktor`: Retrieves from Akkudoktor.net.
- `ElecPriceEnergyCharts`: Retrieves from Energy-Charts.info.
- `ElecPriceImport`: Imports from a file or JSON string.
- `charges_kwh`: Electricity price charges (€/kWh).
- `vat_rate`: VAT rate factor applied to electricity price when charges are used (default: 1.19).
- `provider_settings.import_file_path`: Path to the file to import electricity price forecast data from.
- `provider_settings.import_json`: JSON string, dictionary of electricity price forecast value lists.
@@ -132,6 +135,24 @@ prices by extrapolating historical price data combined with the most recent actu
from Akkudoktor.net. Electricity price charges given in the `charges_kwh` configuration
option are added.
### ElecPriceEnergyCharts Provider
The `ElecPriceEnergyCharts` provider retrieves day-ahead electricity market prices from
[Energy-Charts.info](https://www.Energy-Charts.info). It supports both short-term and extended forecasting by combining
real-time market data with historical price trends.
- For the next 24 hours, market prices are fetched directly from Energy-Charts.info.
- For periods beyond 24 hours, prices are estimated using extrapolation based on historical data and the latest
available market values.
Charges and VAT
- If `charges_kwh` configuration option is greater than 0, the electricity price is calculated as:
`(market price + charges_kwh) * vat_rate` where `vat_rate` is configurable (default: 1.19 for 19% VAT).
- If `charges_kwh` is set to 0, the electricity price is simply: `market_price` (no VAT applied).
**Note:** For the most accurate forecasts, it is recommended to set the `historic_hours` parameter to 840.
### ElecPriceImport Provider
The `ElecPriceImport` provider is designed to import electricity prices from a file or a JSON
@@ -164,6 +185,7 @@ Configuration options:
- `provider`: Load provider id of provider to be used.
- `LoadAkkudoktor`: Retrieves from local database.
- `LoadVrm`: Retrieves data from the VRM API by Victron Energy.
- `LoadImport`: Imports from a file or JSON string.
- `provider_settings.loadakkudoktor_year_energy`: Yearly energy consumption (kWh).
@@ -176,6 +198,27 @@ The `LoadAkkudoktor` provider retrieves generic load data from a local database
align with the annual energy consumption specified in the `loadakkudoktor_year_energy` configuration
option.
### LoadVrm Provider
The `LoadVrm` provider retrieves load forecast data from the VRM API by Victron Energy.
To receive forecasts, the system data must be configured under Dynamic ESS in the VRM portal.
To query the forecasts, an API token is required, which can also be created in the VRM portal under Preferences.
This token must be stored in the EOS configuration along with the VRM-Installations-ID.
```python
{
"load": {
"provider": "LoadVrm",
"provider_settings": {
"load_vrm_token": "dummy-token",
"load_vrm_idsite": 12345
}
```
The prediction keys for the load forecast data are:
- `load_mean`: Predicted load mean value (W).
### LoadImport Provider
The `LoadImport` provider is designed to import load forecast data from a file or a JSON
@@ -214,6 +257,7 @@ Configuration options:
- `provider`: PVForecast provider id of provider to be used.
- `PVForecastAkkudoktor`: Retrieves from Akkudoktor.net.
- `PVForecastVrm`: Retrieves data from the VRM API by Victron Energy.
- `PVForecastImport`: Imports from a file or JSON string.
- `planes[].surface_tilt`: Tilt angle from horizontal plane. Ignored for two-axis tracking.
@@ -243,7 +287,7 @@ Configuration options:
- `provider_settings.import_file_path`: Path to the file to import PV forecast data from.
- `provider_settings.import_json`: JSON string, dictionary of PV forecast value lists.
------
---
Detailed definitions taken from
[PVGIS](https://joint-research-centre.ec.europa.eu/photovoltaic-geographical-information-system-pvgis/getting-started-pvgis/pvgis-user-manual_en).
@@ -274,13 +318,13 @@ conditions (STC), which are a constant 1000W of solar irradiation per square met
the array, at an array temperature of 25°C. The peak power should be entered in kilowatt-peak (kWp).
If you do not know the declared peak power of your modules but instead know the area of the modules
and the declared conversion efficiency (in percent), you can calculate the peak power as
power = area * efficiency / 100.
power = area \* efficiency / 100.
Bifacial modules: PVGIS doesn't make specific calculations for bifacial modules at present. Users
who wish to explore the possible benefits of this technology can input the power value for Bifacial
Nameplate Irradiance. This can also be can also be estimated from the front side peak power P_STC
value and the bifaciality factor, φ (if reported in the module data sheet) as:
P_BNPI = P_STC \* (1 + φ \* 0.135). NB this bifacial approach is not appropriate for BAPV or BIPV
P_BNPI = P_STC \* (1 + φ \* 0.135). NB this bifacial approach is not appropriate for BAPV or BIPV
installations or for modules mounting on a N-S axis i.e. facing E-W.
- `loss`
@@ -321,7 +365,7 @@ represent equal angular distance around the horizon. For instance, if you have 3
point is due north, the next is 10 degrees east of north, and so on, until the last point, 10
degrees west of north.
------
---
Most of the configuration options are in line with the
[PVLib](https://pvlib-python.readthedocs.io/en/stable/_modules/pvlib/iotools/pvgis.html) definition for PVGIS data.
@@ -337,7 +381,7 @@ Tilt angle from horizontal plane.
Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180,
west=270). This is offset 180 degrees from the convention used by PVGIS.
------
---
### PVForecastAkkudoktor Provider
@@ -374,34 +418,56 @@ Example:
"surface_azimuth": -10,
"surface_tilt": 7,
"userhorizon": [20, 27, 22, 20],
"inverter_paco": 10000,
"inverter_paco": 10000
},
{
"peakpower": 4.8,
"surface_azimuth": -90,
"surface_tilt": 7,
"userhorizon": [30, 30, 30, 50],
"inverter_paco": 10000,
"inverter_paco": 10000
},
{
"peakpower": 1.4,
"surface_azimuth": -40,
"surface_tilt": 60,
"userhorizon": [60, 30, 0, 30],
"inverter_paco": 2000,
"inverter_paco": 2000
},
{
"peakpower": 1.6,
"surface_azimuth": 5,
"surface_tilt": 45,
"userhorizon": [45, 25, 30, 60],
"inverter_paco": 1400,
"inverter_paco": 1400
}
]
}
}
```
### PVForecastVrm Provider
The `PVForecastVrm` provider retrieves pv power forecast data from the VRM API by Victron Energy.
To receive forecasts, the system data must be configured under Dynamic ESS in the VRM portal.
To query the forecasts, an API token is required, which can also be created in the VRM portal under Preferences.
This token must be stored in the EOS configuration along with the VRM-Installations-ID.
```python
{
"pvforecast": {
"provider": "PVForecastVrm",
"provider_settings": {
"pvforecast_vrm_token": "dummy-token",
"pvforecast_vrm_idsite": 12345
}
}
```
The prediction keys for the PV forecast data are:
- `pvforecast_dc_power`: Total DC power (W).
### PVForecastImport Provider
The `PVForecastImport` provider is designed to import PV forecast data from a file or a JSON
@@ -410,8 +476,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).
- `pvforecast_ac_power`: Total AC power (W).
- `pvforecast_dc_power`: Total DC power (W).
The PV forecast data must be provided in one of the formats described in
<project:#prediction-import-providers>. The data source can be given in the

View File

@@ -8,20 +8,42 @@
```{toctree}
:maxdepth: 2
:caption: 'Contents:'
:caption: Overview
akkudoktoreos/introduction.md
```
```{toctree}
:maxdepth: 2
:caption: Tutorials
welcome.md
akkudoktoreos/about.md
develop/getting_started.md
```
```{toctree}
:maxdepth: 2
:caption: How-To Guides
develop/CONTRIBUTING.md
```
```{toctree}
:maxdepth: 2
:caption: Reference
akkudoktoreos/architecture.md
akkudoktoreos/configuration.md
akkudoktoreos/optimization.md
akkudoktoreos/prediction.md
akkudoktoreos/measurement.md
akkudoktoreos/integration.md
akkudoktoreos/logging.md
akkudoktoreos/serverapi.md
akkudoktoreos/api.rst
```
## Indices and tables

File diff suppressed because it is too large Load Diff

View File

@@ -43,12 +43,18 @@ profile = "black"
[tool.ruff]
line-length = 100
exclude = [
"tests",
"scripts",
]
output-format = "full"
[tool.ruff.lint]
select = [
"F", # Enable all `Pyflakes` rules.
"D", # Enable all `pydocstyle` rules, limiting to those that adhere to the
# Google convention via `convention = "google"`, below.
"S", # Enable all `flake8-bandit` rules.
]
ignore = [
# Prevent errors due to ruff false positives

View File

@@ -1,13 +1,15 @@
-r requirements.txt
gitpython==3.1.44
gitlint==0.19.1
GitPython==3.1.45
myst-parser==4.0.1
sphinx==8.2.3
sphinx_rtd_theme==3.0.2
sphinx-tabs==3.4.7
pytest==8.3.5
pytest-cov==6.0.0
pymarkdownlnt==0.9.32
pytest==8.4.2
pytest-cov==7.0.0
pytest-xprocess==1.0.2
pre-commit
mypy==1.15.0
types-requests==2.32.0.20250306
pandas-stubs==2.2.3.250308
mypy==1.18.2
types-requests==2.32.4.20250913
pandas-stubs==2.3.2.250926

View File

@@ -1,24 +1,25 @@
cachebox==4.4.2
numpy==2.2.4
numpydantic==1.6.8
matplotlib==3.10.1
fastapi[standard]==0.115.11
python-fasthtml==0.12.4
MonsterUI==0.0.29
cachebox==5.0.2
numpy==2.3.3
numpydantic==1.6.11
matplotlib==3.10.6
fastapi[standard]==0.115.14
python-fasthtml==0.12.29
MonsterUI==1.0.29
markdown-it-py==3.0.0
mdit-py-plugins==0.4.2
bokeh==3.6.3
uvicorn==0.34.0
scikit-learn==1.6.1
timezonefinder==6.5.8
deap==1.4.2
requests==2.32.3
pandas==2.2.3
pendulum==3.0.0
platformdirs==4.3.7
psutil==6.1.1
pvlib==0.12.0
pydantic==2.10.6
statsmodels==0.14.4
pydantic-settings==2.7.0
mdit-py-plugins==0.5.0
bokeh==3.8.0
uvicorn==0.36.0
scikit-learn==1.7.2
timezonefinder==7.0.2
deap==1.4.3
requests==2.32.5
pandas==2.3.2
pendulum==3.1.0
platformdirs==4.4.0
psutil==7.1.0
pvlib==0.13.1
pydantic==2.11.9
statsmodels==0.14.5
pydantic-settings==2.11.0
linkify-it-py==2.0.3
loguru==0.7.3

View File

@@ -4,22 +4,20 @@
import argparse
import json
import os
import re
import sys
import textwrap
from pathlib import Path
from typing import Any, Union
from loguru import logger
from pydantic.fields import ComputedFieldInfo, FieldInfo
from pydantic_core import PydanticUndefined
from akkudoktoreos.config.config import ConfigEOS, GeneralSettings, get_config
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.core.pydantic import PydanticBaseModel
from akkudoktoreos.utils.docs import get_model_structure_from_examples
logger = get_logger(__name__)
documented_types: set[PydanticBaseModel] = set()
undocumented_types: dict[PydanticBaseModel, tuple[str, list[str]]] = dict()
@@ -145,6 +143,7 @@ def generate_config_table_md(
field_type = field_info.annotation if regular_field else field_info.return_type
default_value = get_default_value(field_info, regular_field)
description = field_info.description if field_info.description else "-"
deprecated = field_info.deprecated if field_info.deprecated else None
read_only = "rw" if regular_field else "ro"
type_name = get_type_name(field_type)
@@ -154,6 +153,11 @@ def generate_config_table_md(
env_entry = f"| `{prefix}{config_name}` "
else:
env_entry = "| "
if deprecated:
if isinstance(deprecated, bool):
description = "Deprecated!"
else:
description = deprecated
table += f"| {field_name} {env_entry}| `{type_name}` | `{read_only}` | `{default_value}` | {description} |\n"
inner_types: dict[PydanticBaseModel, tuple[str, list[str]]] = dict()
@@ -259,7 +263,7 @@ def generate_config_md(config_eos: ConfigEOS) -> str:
markdown = "# Configuration Table\n\n"
# Generate tables for each top level config
for field_name, field_info in config_eos.model_fields.items():
for field_name, field_info in config_eos.__class__.model_fields.items():
field_type = field_info.annotation
markdown += generate_config_table_md(
field_type, [field_name], f"EOS_{field_name.upper()}__", True
@@ -279,6 +283,13 @@ def generate_config_md(config_eos: ConfigEOS) -> str:
markdown = markdown.rstrip("\n")
markdown += "\n"
# Assure log path does not leak to documentation
markdown = re.sub(
r'(?<=["\'])/[^"\']*/output/eos\.log(?=["\'])',
'/home/user/.local/share/net.akkudoktoreos.net/output/eos.log',
markdown
)
return markdown
@@ -298,7 +309,7 @@ def main():
try:
config_md = generate_config_md(config_eos)
if os.name == "nt":
config_md = config_md.replace("127.0.0.1", "0.0.0.0").replace("\\\\", "/")
config_md = config_md.replace("\\\\", "/")
if args.output_file:
# Write to file
with open(args.output_file, "w", encoding="utf-8", newline="\n") as f:

View File

@@ -42,6 +42,9 @@ def generate_openapi() -> dict:
general = openapi_spec["components"]["schemas"]["ConfigEOS"]["properties"]["general"]["default"]
general["config_file_path"] = "/home/user/.config/net.akkudoktoreos.net/EOS.config.json"
general["config_folder_path"] = "/home/user/.config/net.akkudoktoreos.net"
# Fix file path for logging settings to not show local/test file path
logging = openapi_spec["components"]["schemas"]["ConfigEOS"]["properties"]["logging"]["default"]
logging["file_path"] = "/home/user/.local/share/net.akkudoktoreos.net/output/eos.log"
return openapi_spec
@@ -58,8 +61,6 @@ def main():
try:
openapi_spec = generate_openapi()
openapi_spec_str = json.dumps(openapi_spec, indent=2)
if os.name == "nt":
openapi_spec_str = openapi_spec_str.replace("127.0.0.1", "0.0.0.0")
if args.output_file:
# Write to file
with open(args.output_file, "w", encoding="utf-8", newline="\n") as f:

View File

@@ -286,7 +286,7 @@ def main():
try:
openapi_md = generate_openapi_md()
if os.name == "nt":
openapi_md = openapi_md.replace("127.0.0.1", "0.0.0.0")
openapi_md = openapi_md.replace("127.0.0.1", "127.0.0.1")
if args.output_file:
# Write to file
with open(args.output_file, "w", encoding="utf-8", newline="\n") as f:

View File

@@ -0,0 +1 @@
# Placeholder for gitlint user rules (see https://jorisroovers.com/gitlint/latest/rules/user_defined_rules/).

View File

@@ -12,15 +12,12 @@ import numpy as np
from akkudoktoreos.config.config import get_config
from akkudoktoreos.core.ems import get_ems
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.optimization.genetic import (
OptimizationParameters,
optimization_problem,
)
from akkudoktoreos.prediction.prediction import get_prediction
get_logger(__name__, logging_level="DEBUG")
def prepare_optimization_real_parameters() -> OptimizationParameters:
"""Prepare and return optimization parameters with real world data.

View File

@@ -14,6 +14,7 @@ import shutil
from pathlib import Path
from typing import Any, ClassVar, Optional, Type
from loguru import logger
from platformdirs import user_config_dir, user_data_dir
from pydantic import Field, computed_field
from pydantic_settings import (
@@ -29,9 +30,8 @@ from akkudoktoreos.core.cachesettings import CacheCommonSettings
from akkudoktoreos.core.coreabc import SingletonMixin
from akkudoktoreos.core.decorators import classproperty
from akkudoktoreos.core.emsettings import EnergyManagementCommonSettings
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.core.logsettings import LoggingCommonSettings
from akkudoktoreos.core.pydantic import access_nested_value, merge_models
from akkudoktoreos.core.pydantic import PydanticModelNestedValueMixin, merge_models
from akkudoktoreos.devices.settings import DevicesCommonSettings
from akkudoktoreos.measurement.measurement import MeasurementCommonSettings
from akkudoktoreos.optimization.optimization import OptimizationCommonSettings
@@ -44,8 +44,6 @@ from akkudoktoreos.server.server import ServerCommonSettings
from akkudoktoreos.utils.datetimeutil import to_timezone
from akkudoktoreos.utils.utils import UtilsCommonSettings
logger = get_logger(__name__)
def get_absolute_path(
basepath: Optional[Path | str], subpath: Optional[Path | str]
@@ -80,10 +78,6 @@ class GeneralSettings(SettingsBaseModel):
Properties:
timezone (Optional[str]): Computed time zone string based on the specified latitude
and longitude.
Validators:
validate_latitude (float): Ensures `latitude` is within the range -90 to 90.
validate_longitude (float): Ensures `longitude` is within the range -180 to 180.
"""
_config_folder_path: ClassVar[Optional[Path]] = None
@@ -138,7 +132,7 @@ class GeneralSettings(SettingsBaseModel):
return self._config_file_path
class SettingsEOS(BaseSettings):
class SettingsEOS(BaseSettings, PydanticModelNestedValueMixin):
"""Settings for all EOS.
Used by updating the configuration with specific settings only.
@@ -283,6 +277,16 @@ class ConfigEOS(SingletonMixin, SettingsEOSDefaults):
ENCODING: ClassVar[str] = "UTF-8"
CONFIG_FILE_NAME: ClassVar[str] = "EOS.config.json"
def __hash__(self) -> int:
# ConfigEOS is a singleton
return hash("config_eos")
def __eq__(self, other: Any) -> bool:
if not isinstance(other, ConfigEOS):
return False
# ConfigEOS is a singleton
return True
@classmethod
def settings_customise_sources(
cls,
@@ -376,6 +380,15 @@ class ConfigEOS(SingletonMixin, SettingsEOSDefaults):
def _setup(self, *args: Any, **kwargs: Any) -> None:
"""Re-initialize global settings."""
# Check for config file content/ version type
config_file, exists = self._get_config_file_path()
if exists:
with config_file.open("r", encoding="utf-8", newline=None) as f_config:
config_txt = f_config.read()
if '"directories": {' in config_txt or '"server_eos_host": ' in config_txt:
error_msg = f"Configuration file '{config_file}' is outdated. Please remove or update manually."
logger.error(error_msg)
raise ValueError(error_msg)
# Assure settings base knows EOS configuration
SettingsBaseModel.config = self
# (Re-)load settings
@@ -394,7 +407,9 @@ class ConfigEOS(SingletonMixin, SettingsEOSDefaults):
ValueError: If the `settings` is not a `SettingsEOS` instance.
"""
if not isinstance(settings, SettingsEOS):
raise ValueError(f"Settings must be an instance of SettingsEOS: '{settings}'.")
error_msg = f"Settings must be an instance of SettingsEOS: '{settings}'."
logger.error(error_msg)
raise ValueError(error_msg)
self.merge_settings_from_dict(settings.model_dump(exclude_none=True, exclude_unset=True))
@@ -426,32 +441,6 @@ class ConfigEOS(SingletonMixin, SettingsEOSDefaults):
"""
self._setup()
def set_config_value(self, path: str, value: Any) -> None:
"""Set a configuration value based on the provided path.
Supports string paths (with '/' separators) or sequence paths (list/tuple).
Trims leading and trailing '/' from string paths.
Args:
path (str): The path to the configuration key (e.g., "key1/key2/key3" or key1/key2/0).
value (Any]): The value to set.
"""
access_nested_value(self, path, True, value)
def get_config_value(self, path: str) -> Any:
"""Get a configuration value based on the provided path.
Supports string paths (with '/' separators) or sequence paths (list/tuple).
Trims leading and trailing '/' from string paths.
Args:
path (str): The path to the configuration key (e.g., "key1/key2/key3" or key1/key2/0).
Returns:
Any: The retrieved value.
"""
return access_nested_value(self, path, False)
def _create_initial_config_file(self) -> None:
if self.general.config_file_path and not self.general.config_file_path.exists():
self.general.config_file_path.parent.mkdir(parents=True, exist_ok=True)
@@ -497,10 +486,10 @@ class ConfigEOS(SingletonMixin, SettingsEOSDefaults):
@classmethod
def _get_config_file_path(cls) -> tuple[Path, bool]:
"""Finds the a valid configuration file or returns the desired path for a new config file.
"""Find a valid configuration file or return the desired path for a new config file.
Returns:
tuple[Path, bool]: The path to the configuration directory and if there is already a config file there
tuple[Path, bool]: The path to the configuration file and if there is already a config file there
"""
config_dirs = []
env_base_dir = os.getenv(cls.EOS_DIR)
@@ -529,7 +518,7 @@ class ConfigEOS(SingletonMixin, SettingsEOSDefaults):
if not self.general.config_file_path:
raise ValueError("Configuration file path unknown.")
with self.general.config_file_path.open("w", encoding="utf-8", newline="\n") as f_out:
json_str = super().model_dump_json()
json_str = super().model_dump_json(indent=4)
f_out.write(json_str)
def update(self) -> None:

View File

@@ -27,17 +27,14 @@ from typing import (
)
import cachebox
from loguru import logger
from pendulum import DateTime, Duration
from pydantic import Field
from akkudoktoreos.core.coreabc import ConfigMixin, SingletonMixin
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.core.pydantic import PydanticBaseModel
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime, to_duration
logger = get_logger(__name__)
# ---------------------------------
# In-Memory Caching Functionality
# ---------------------------------
@@ -956,7 +953,7 @@ def cache_in_file(
logger.debug("Used cache file for function: " + func.__name__)
cache_file.seek(0)
if "b" in mode:
result = pickle.load(cache_file)
result = pickle.load(cache_file) # noqa: S301
else:
result = cache_file.read()
except Exception as e:

View File

@@ -13,13 +13,10 @@ Classes:
import threading
from typing import Any, ClassVar, Dict, Optional, Type
from loguru import logger
from pendulum import DateTime
from pydantic import computed_field
from akkudoktoreos.core.logging import get_logger
logger = get_logger(__name__)
config_eos: Any = None
measurement_eos: Any = None
prediction_eos: Any = None

View File

@@ -19,6 +19,7 @@ from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union, over
import numpy as np
import pandas as pd
import pendulum
from loguru import logger
from numpydantic import NDArray, Shape
from pendulum import DateTime, Duration
from pydantic import (
@@ -31,7 +32,6 @@ from pydantic import (
)
from akkudoktoreos.core.coreabc import ConfigMixin, SingletonMixin, StartMixin
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.core.pydantic import (
PydanticBaseModel,
PydanticDateTimeData,
@@ -39,8 +39,6 @@ from akkudoktoreos.core.pydantic import (
)
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime, to_duration
logger = get_logger(__name__)
class DataBase(ConfigMixin, StartMixin, PydanticBaseModel):
"""Base class for handling generic data.

View File

@@ -1,10 +1,6 @@
from collections.abc import Callable
from typing import Any, Optional
from akkudoktoreos.core.logging import get_logger
logger = get_logger(__name__)
class classproperty:
"""A decorator to define a read-only property at the class level.
@@ -34,7 +30,7 @@ class classproperty:
argument and returns a value.
Raises:
AssertionError: If `fget` is not defined when `__get__` is called.
RuntimeError: If `fget` is not defined when `__get__` is called.
"""
def __init__(self, fget: Callable[[Any], Any]) -> None:
@@ -43,5 +39,6 @@ class classproperty:
def __get__(self, _: Any, owner_cls: Optional[type[Any]] = None) -> Any:
if owner_cls is None:
return self
assert self.fget is not None
if self.fget is None:
raise RuntimeError("'fget' not defined when `__get__` is called")
return self.fget(owner_cls)

View File

@@ -1,6 +1,8 @@
import traceback
from typing import Any, ClassVar, Optional
import numpy as np
from loguru import logger
from numpydantic import NDArray, Shape
from pendulum import DateTime
from pydantic import ConfigDict, Field, computed_field, field_validator, model_validator
@@ -8,7 +10,6 @@ from typing_extensions import Self
from akkudoktoreos.core.cache import CacheUntilUpdateStore
from akkudoktoreos.core.coreabc import ConfigMixin, PredictionMixin, SingletonMixin
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.core.pydantic import ParametersBaseModel, PydanticBaseModel
from akkudoktoreos.devices.battery import Battery
from akkudoktoreos.devices.generic import HomeAppliance
@@ -16,8 +17,6 @@ from akkudoktoreos.devices.inverter import Inverter
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime
from akkudoktoreos.utils.utils import NumpyEncoder
logger = get_logger(__name__)
class EnergyManagementParameters(ParametersBaseModel):
pv_prognose_wh: list[float] = Field(
@@ -282,6 +281,8 @@ class EnergyManagement(SingletonMixin, ConfigMixin, PredictionMixin, PydanticBas
self.prediction.update_data(force_enable=force_enable, force_update=force_update)
# TODO: Create optimisation problem that calls into devices.update_data() for simulations.
logger.info("Energy management run (crippled version - prediction update only)")
def manage_energy(self) -> None:
"""Repeating task for managing energy.
@@ -301,26 +302,28 @@ class EnergyManagement(SingletonMixin, ConfigMixin, PredictionMixin, PydanticBas
Note: The task maintains the interval even if some intervals are missed.
"""
current_datetime = to_datetime()
interval = self.config.ems.interval # interval maybe changed in between
if EnergyManagement._last_datetime is None:
# Never run before
try:
# Try to run a first energy management. May fail due to config incomplete.
self.run()
# Remember energy run datetime.
EnergyManagement._last_datetime = current_datetime
# Try to run a first energy management. May fail due to config incomplete.
self.run()
except Exception as e:
message = f"EOS init: {e}"
trace = "".join(traceback.TracebackException.from_exception(e).format())
message = f"EOS init: {e}\n{trace}"
logger.error(message)
return
if self.config.ems.interval is None or self.config.ems.interval == float("nan"):
if interval is None or interval == float("nan"):
# No Repetition
return
if (
compare_datetimes(current_datetime, self._last_datetime).time_diff
< self.config.ems.interval
compare_datetimes(current_datetime, EnergyManagement._last_datetime).time_diff
< interval
):
# Wait for next run
return
@@ -328,15 +331,16 @@ class EnergyManagement(SingletonMixin, ConfigMixin, PredictionMixin, PydanticBas
try:
self.run()
except Exception as e:
message = f"EOS run: {e}"
trace = "".join(traceback.TracebackException.from_exception(e).format())
message = f"EOS run: {e}\n{trace}"
logger.error(message)
# Remember the energy management run - keep on interval even if we missed some intervals
while (
compare_datetimes(current_datetime, EnergyManagement._last_datetime).time_diff
>= self.config.ems.interval
>= interval
):
EnergyManagement._last_datetime.add(seconds=self.config.ems.interval)
EnergyManagement._last_datetime = EnergyManagement._last_datetime.add(seconds=interval)
def set_start_hour(self, start_hour: Optional[int] = None) -> None:
"""Sets start datetime to given hour.
@@ -390,7 +394,8 @@ class EnergyManagement(SingletonMixin, ConfigMixin, PredictionMixin, PydanticBas
# Fetch objects
battery = self.battery
assert battery # to please mypy
if battery is None:
raise ValueError(f"battery not set: {battery}")
ev = self.ev
home_appliance = self.home_appliance
inverter = self.inverter

View File

@@ -1,20 +1,3 @@
"""Abstract and base classes for logging."""
import logging
def logging_str_to_level(level_str: str) -> int:
"""Convert log level string to logging level."""
if level_str == "DEBUG":
level = logging.DEBUG
elif level_str == "INFO":
level = logging.INFO
elif level_str == "WARNING":
level = logging.WARNING
elif level_str == "CRITICAL":
level = logging.CRITICAL
elif level_str == "ERROR":
level = logging.ERROR
else:
raise ValueError(f"Unknown loggin level: {level_str}")
return level
LOGGING_LEVELS: list[str] = ["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]

View File

@@ -1,95 +1,241 @@
"""Utility functions for handling logging tasks.
Functions:
----------
- get_logger: Creates and configures a logger with console and optional rotating file logging.
Example usage:
--------------
# Logger setup
>>> logger = get_logger(__name__, log_file="app.log", logging_level="DEBUG")
>>> logger.info("Logging initialized.")
Notes:
------
- The logger supports rotating log files to prevent excessive log file size.
"""
"""Utility for configuring Loguru loggers."""
import json
import logging as pylogging
import os
from logging.handlers import RotatingFileHandler
from typing import Optional
import re
import sys
from pathlib import Path
from types import FrameType
from typing import Any, List, Optional
from akkudoktoreos.core.logabc import logging_str_to_level
import pendulum
from loguru import logger
from akkudoktoreos.core.logabc import LOGGING_LEVELS
def get_logger(
name: str,
log_file: Optional[str] = None,
logging_level: Optional[str] = None,
max_bytes: int = 5000000,
backup_count: int = 5,
) -> pylogging.Logger:
"""Creates and configures a logger with a given name.
class InterceptHandler(pylogging.Handler):
"""A logging handler that redirects standard Python logging messages to Loguru.
The logger supports logging to both the console and an optional log file. File logging is
handled by a rotating file handler to prevent excessive log file size.
This handler ensures consistency between the `logging` module and Loguru by intercepting
logs sent to the standard logging system and re-emitting them through Loguru with proper
formatting and context (including exception info and call depth).
Attributes:
loglevel_mapping (dict): Mapping from standard logging levels to Loguru level names.
"""
loglevel_mapping: dict[int, str] = {
50: "CRITICAL",
40: "ERROR",
30: "WARNING",
20: "INFO",
10: "DEBUG",
5: "TRACE",
0: "NOTSET",
}
def emit(self, record: pylogging.LogRecord) -> None:
"""Emits a logging record by forwarding it to Loguru with preserved metadata.
Args:
record (logging.LogRecord): A record object containing log message and metadata.
"""
try:
level = logger.level(record.levelname).name
except AttributeError:
level = self.loglevel_mapping.get(record.levelno, "INFO")
frame: Optional[FrameType] = pylogging.currentframe()
depth: int = 2
while frame and frame.f_code.co_filename == pylogging.__file__:
frame = frame.f_back
depth += 1
log = logger.bind(request_id="app")
log.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
console_handler_id = None
file_handler_id = None
def track_logging_config(config_eos: Any, path: str, old_value: Any, value: Any) -> None:
"""Track logging config changes."""
global console_handler_id, file_handler_id
if not path.startswith("logging"):
raise ValueError(f"Logging shall not track '{path}'")
if not config_eos.logging.console_level:
# No value given - check environment value - may also be None
config_eos.logging.console_level = os.getenv("EOS_LOGGING__LEVEL")
if not config_eos.logging.file_level:
# No value given - check environment value - may also be None
config_eos.logging.file_level = os.getenv("EOS_LOGGING__LEVEL")
# Remove handlers
if console_handler_id:
try:
logger.remove(console_handler_id)
except Exception as e:
logger.debug("Exception on logger.remove: {}", e, exc_info=True)
console_handler_id = None
if file_handler_id:
try:
logger.remove(file_handler_id)
except Exception as e:
logger.debug("Exception on logger.remove: {}", e, exc_info=True)
file_handler_id = None
# Create handlers with new configuration
# Always add console handler
if config_eos.logging.console_level not in LOGGING_LEVELS:
logger.error(
f"Invalid console log level '{config_eos.logging.console_level} - forced to INFO'."
)
config_eos.logging.console_level = "INFO"
console_handler_id = logger.add(
sys.stderr,
enqueue=True,
backtrace=True,
level=config_eos.logging.console_level,
# format=_console_format
)
# Add file handler
if config_eos.logging.file_level and config_eos.logging.file_path:
if config_eos.logging.file_level not in LOGGING_LEVELS:
logger.error(
f"Invalid file log level '{config_eos.logging.console_level}' - forced to INFO."
)
config_eos.logging.file_level = "INFO"
file_handler_id = logger.add(
sink=config_eos.logging.file_path,
rotation="100 MB",
retention="3 days",
enqueue=True,
backtrace=True,
level=config_eos.logging.file_level,
serialize=True, # JSON dict formatting
# format=_file_format
)
# Redirect standard logging to Loguru
pylogging.basicConfig(handlers=[InterceptHandler()], level=0)
# Redirect uvicorn and fastapi logging to Loguru
pylogging.getLogger("uvicorn.access").handlers = [InterceptHandler()]
for pylogger_name in ["uvicorn", "uvicorn.error", "fastapi"]:
pylogger = pylogging.getLogger(pylogger_name)
pylogger.handlers = [InterceptHandler()]
pylogger.propagate = False
logger.info(
f"Logger reconfigured - console: {config_eos.logging.console_level}, file: {config_eos.logging.file_level}."
)
def read_file_log(
log_path: Path,
limit: int = 100,
level: Optional[str] = None,
contains: Optional[str] = None,
regex: Optional[str] = None,
from_time: Optional[str] = None,
to_time: Optional[str] = None,
tail: bool = False,
) -> List[dict]:
"""Read and filter structured log entries from a JSON-formatted log file.
Args:
name (str): The name of the logger, typically `__name__` from the calling module.
log_file (Optional[str]): Path to the log file for file logging. If None, no file logging is done.
logging_level (Optional[str]): Logging level (e.g., "INFO", "DEBUG"). Defaults to "INFO".
max_bytes (int): Maximum size in bytes for log file before rotation. Defaults to 5 MB.
backup_count (int): Number of backup log files to keep. Defaults to 5.
log_path (Path): Path to the JSON-formatted log file.
limit (int, optional): Maximum number of log entries to return. Defaults to 100.
level (Optional[str], optional): Filter logs by log level (e.g., "INFO", "ERROR"). Defaults to None.
contains (Optional[str], optional): Filter logs that contain this substring in their message. Case-insensitive. Defaults to None.
regex (Optional[str], optional): Filter logs whose message matches this regular expression. Defaults to None.
from_time (Optional[str], optional): ISO 8601 datetime string to filter logs not earlier than this time. Defaults to None.
to_time (Optional[str], optional): ISO 8601 datetime string to filter logs not later than this time. Defaults to None.
tail (bool, optional): If True, read the last lines of the file (like `tail -n`). Defaults to False.
Returns:
logging.Logger: Configured logger instance.
List[dict]: A list of filtered log entries as dictionaries.
Example:
logger = get_logger(__name__, log_file="app.log", logging_level="DEBUG")
logger.info("Application started")
Raises:
FileNotFoundError: If the log file does not exist.
ValueError: If the datetime strings are invalid or improperly formatted.
Exception: For other unforeseen I/O or parsing errors.
"""
# Create a logger with the specified name
logger = pylogging.getLogger(name)
logger.propagate = True
# This is already supported by pydantic-settings in LoggingCommonSettings, however in case
# loading the config itself fails and to set the level before we load the config, we set it here manually.
if logging_level is None and (env_level := os.getenv("EOS_LOGGING__LEVEL")) is not None:
logging_level = env_level
if logging_level is not None:
level = logging_str_to_level(logging_level)
logger.setLevel(level)
if not log_path.exists():
raise FileNotFoundError("Log file not found")
# The log message format
formatter = pylogging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
try:
from_dt = pendulum.parse(from_time) if from_time else None
to_dt = pendulum.parse(to_time) if to_time else None
except Exception as e:
raise ValueError(f"Invalid date/time format: {e}")
# Prevent loggers from being added multiple times
# There may already be a logger from pytest
if not logger.handlers:
# Create a console handler with a standard output stream
console_handler = pylogging.StreamHandler()
if logging_level is not None:
console_handler.setLevel(level)
console_handler.setFormatter(formatter)
regex_pattern = re.compile(regex) if regex else None
# Add the console handler to the logger
logger.addHandler(console_handler)
def matches_filters(log: dict) -> bool:
if level and log.get("level", {}).get("name") != level.upper():
return False
if contains and contains.lower() not in log.get("message", "").lower():
return False
if regex_pattern and not regex_pattern.search(log.get("message", "")):
return False
if from_dt or to_dt:
try:
log_time = pendulum.parse(log["time"])
except Exception:
return False
if from_dt and log_time < from_dt:
return False
if to_dt and log_time > to_dt:
return False
return True
if log_file and len(logger.handlers) < 2: # We assume a console logger to be the first logger
# If a log file path is specified, create a rotating file handler
matched_logs = []
lines: list[str] = []
# Ensure the log directory exists
log_dir = os.path.dirname(log_file)
if log_dir and not os.path.exists(log_dir):
os.makedirs(log_dir)
if tail:
with log_path.open("rb") as f:
f.seek(0, 2)
end = f.tell()
buffer = bytearray()
pointer = end
# Create a rotating file handler
file_handler = RotatingFileHandler(log_file, maxBytes=max_bytes, backupCount=backup_count)
if logging_level is not None:
file_handler.setLevel(level)
file_handler.setFormatter(formatter)
while pointer > 0 and len(lines) < limit * 5:
pointer -= 1
f.seek(pointer)
byte = f.read(1)
if byte == b"\n":
if buffer:
line = buffer[::-1].decode("utf-8", errors="ignore")
lines.append(line)
buffer.clear()
else:
buffer.append(byte[0])
if buffer:
line = buffer[::-1].decode("utf-8", errors="ignore")
lines.append(line)
lines = lines[::-1]
else:
with log_path.open("r", encoding="utf-8", newline=None) as f_txt:
lines = f_txt.readlines()
# Add the file handler to the logger
logger.addHandler(file_handler)
for line in lines:
if not line.strip():
continue
try:
log = json.loads(line)
except json.JSONDecodeError:
continue
if matches_filters(log):
matched_logs.append(log)
if len(matched_logs) >= limit:
break
return logger
return matched_logs

View File

@@ -3,13 +3,13 @@
Kept in an extra module to avoid cyclic dependencies on package import.
"""
import logging
from pathlib import Path
from typing import Optional
from pydantic import Field, computed_field, field_validator
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.logabc import logging_str_to_level
from akkudoktoreos.core.logabc import LOGGING_LEVELS
class LoggingCommonSettings(SettingsBaseModel):
@@ -17,27 +17,47 @@ class LoggingCommonSettings(SettingsBaseModel):
level: Optional[str] = Field(
default=None,
description="EOS default logging level.",
examples=["INFO", "DEBUG", "WARNING", "ERROR", "CRITICAL"],
deprecated="This is deprecated. Use console_level and file_level instead.",
)
# Validators
@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:
return None
level = logging_str_to_level(value)
logging.getLogger().setLevel(level)
return value
console_level: Optional[str] = Field(
default=None,
description="Logging level when logging to console.",
examples=LOGGING_LEVELS,
)
file_level: Optional[str] = Field(
default=None,
description="Logging level when logging to file.",
examples=LOGGING_LEVELS,
)
# Computed fields
@computed_field # type: ignore[prop-decorator]
@property
def root_level(self) -> str:
"""Root logger logging level."""
level = logging.getLogger().getEffectiveLevel()
level_name = logging.getLevelName(level)
return level_name
def file_path(self) -> Optional[Path]:
"""Computed log file path based on data output path."""
try:
path = SettingsBaseModel.config.general.data_output_path / "eos.log"
except:
# Config may not be fully set up
path = None
return path
# Validators
@field_validator("console_level", "file_level", mode="after")
@classmethod
def validate_level(cls, value: Optional[str]) -> Optional[str]:
"""Validate logging level string."""
if value is None:
# Nothing to set
return None
if isinstance(value, str):
level = value.upper()
if level == "NONE":
return None
if level not in LOGGING_LEVELS:
raise ValueError(f"Logging level {value} not supported")
value = level
else:
raise TypeError(f"Invalid {type(value)} of logging level {value}")
return value

View File

@@ -12,20 +12,35 @@ Key Features:
pandas DataFrames and Series with datetime indexes.
"""
import inspect
import json
import re
import uuid
import weakref
from copy import deepcopy
from typing import Any, Dict, List, Optional, Type, Union
from typing import (
Any,
Callable,
Dict,
List,
Optional,
Type,
Union,
get_args,
get_origin,
)
from zoneinfo import ZoneInfo
import pandas as pd
import pendulum
from loguru import logger
from pandas.api.types import is_datetime64_any_dtype
from pydantic import (
AwareDatetime,
BaseModel,
ConfigDict,
Field,
PrivateAttr,
RootModel,
TypeAdapter,
ValidationError,
@@ -35,6 +50,10 @@ from pydantic import (
from akkudoktoreos.utils.datetimeutil import to_datetime, to_duration
# Global weakref dictionary to hold external state per model instance
# Used as a workaround for PrivateAttr not working in e.g. Mixin Classes
_model_private_state: "weakref.WeakKeyDictionary[Union[PydanticBaseModel, PydanticModelNestedValueMixin], Dict[str, Any]]" = weakref.WeakKeyDictionary()
def merge_models(source: BaseModel, update_dict: dict[str, Any]) -> dict[str, Any]:
def deep_update(source_dict: dict[str, Any], update_dict: dict[str, Any]) -> dict[str, Any]:
@@ -51,70 +70,6 @@ def merge_models(source: BaseModel, update_dict: dict[str, Any]) -> dict[str, An
return merged_dict
def access_nested_value(
model: BaseModel, path: str, setter: bool, value: Optional[Any] = None
) -> Any:
"""Get or set a nested model value based on the provided path.
Supports string paths (with '/' separators) or sequence paths (list/tuple).
Trims leading and trailing '/' from string paths.
Args:
model (BaseModel): The model object for partial assignment.
path (str): The path to the model key (e.g., "key1/key2/key3" or key1/key2/0).
setter (bool): True to set value at path, False to return value at path.
value (Optional[Any]): The value to set.
Returns:
Any: The retrieved value if acting as a getter, or None if setting a value.
"""
path_elements = path.strip("/").split("/")
cfg: Any = model
parent: BaseModel = model
model_key: str = ""
for i, key in enumerate(path_elements):
is_final_key = i == len(path_elements) - 1
if isinstance(cfg, list):
try:
idx = int(key)
if is_final_key:
if not setter: # Getter
return cfg[idx]
else: # Setter
new_list = list(cfg)
new_list[idx] = value
# Trigger validation
setattr(parent, model_key, new_list)
else:
cfg = cfg[idx]
except ValidationError as e:
raise ValueError(f"Error updating model: {e}") from e
except (ValueError, IndexError) as e:
raise IndexError(f"Invalid list index at {path}: {key}") from e
elif isinstance(cfg, BaseModel):
parent = cfg
model_key = key
if is_final_key:
if not setter: # Getter
return getattr(cfg, key)
else: # Setter
try:
# Verification also if nested value is provided opposed to just setattr
# Will merge partial assignment
cfg = cfg.__pydantic_validator__.validate_assignment(cfg, key, value)
except Exception as e:
raise ValueError(f"Error updating model: {e}") from e
else:
cfg = getattr(cfg, key)
else:
raise KeyError(f"Key '{key}' not found in model.")
class PydanticTypeAdapterDateTime(TypeAdapter[pendulum.DateTime]):
"""Custom type adapter for Pendulum DateTime fields."""
@@ -146,11 +101,538 @@ class PydanticTypeAdapterDateTime(TypeAdapter[pendulum.DateTime]):
return bool(re.match(iso8601_pattern, value))
class PydanticBaseModel(BaseModel):
"""Base model class with automatic serialization and deserialization of `pendulum.DateTime` fields.
class PydanticModelNestedValueMixin:
"""A mixin providing methods to get, set and track nested values within a Pydantic model.
This model serializes pendulum.DateTime objects to ISO 8601 strings and
deserializes ISO 8601 strings to pendulum.DateTime objects.
The methods use a '/'-separated path to denote the nested values.
Supports handling `Optional`, `List`, and `Dict` types, ensuring correct initialization of
missing attributes.
Example:
class Address(PydanticBaseModel):
city: str
class User(PydanticBaseModel):
name: str
address: Address
def on_city_change(old, new, path):
print(f"{path}: {old} -> {new}")
user = User(name="Alice", address=Address(city="NY"))
user.track_nested_value("address/city", on_city_change)
user.set_nested_value("address/city", "LA") # triggers callback
"""
def track_nested_value(self, path: str, callback: Callable[[Any, str, Any, Any], None]) -> None:
"""Register a callback for a specific path (or subtree).
Callback triggers if set path is equal or deeper.
Args:
path (str): '/'-separated path to track.
callback (callable): Function called as callback(model_instance, set_path, old_value, new_value).
"""
try:
self._validate_path_structure(path)
pass
except:
raise ValueError(f"Path '{path}' is invalid")
path = path.strip("/")
# Use private data workaround
# Should be:
# _nested_value_callbacks: dict[str, list[Callable[[str, Any, Any], None]]]
# = PrivateAttr(default_factory=dict)
nested_value_callbacks = get_private_attr(self, "nested_value_callbacks", dict())
if path not in nested_value_callbacks:
nested_value_callbacks[path] = []
nested_value_callbacks[path].append(callback)
set_private_attr(self, "nested_value_callbacks", nested_value_callbacks)
logger.debug("Nested value callbacks {}", nested_value_callbacks)
def _validate_path_structure(self, path: str) -> None:
"""Validate that a '/'-separated path is structurally valid for this model.
Checks that each segment of the path corresponds to a field or index in the model's type structure,
without requiring that all intermediate values are currently initialized. This method is intended
to ensure that the path could be valid for nested access or assignment, according to the model's
class definition.
Args:
path (str): The '/'-separated attribute/index path to validate (e.g., "address/city" or "items/0/value").
Raises:
ValueError: If any segment of the path does not correspond to a valid field in the model,
or an invalid transition is made (such as an attribute on a non-model).
Example:
class Address(PydanticBaseModel):
city: str
class User(PydanticBaseModel):
name: str
address: Address
user = User(name="Alice", address=Address(city="NY"))
user._validate_path_structure("address/city") # OK
user._validate_path_structure("address/zipcode") # Raises ValueError
"""
path_elements = path.strip("/").split("/")
# The model we are currently working on
model: Any = self
# The model we get the type information from. It is a pydantic BaseModel
parent: BaseModel = model
# The field that provides type information for the current key
# Fields may have nested types that translates to a sequence of keys, not just one
# - my_field: Optional[list[OtherModel]] -> e.g. "myfield/0" for index 0
# parent_key = ["myfield",] ... ["myfield", "0"]
# parent_key_types = [list, OtherModel]
parent_key: list[str] = []
parent_key_types: list = []
for i, key in enumerate(path_elements):
is_final_key = i == len(path_elements) - 1
# Add current key to parent key to enable nested type tracking
parent_key.append(key)
# Get next value
next_value = None
if isinstance(model, BaseModel):
# Track parent and key for possible assignment later
parent = model
parent_key = [
key,
]
parent_key_types = self._get_key_types(model.__class__, key)
# If this is the final key, set the value
if is_final_key:
return
# Attempt to access the next attribute, handling None values
next_value = getattr(model, key, None)
# Handle missing values (initialize dict/list/model if necessary)
if next_value is None:
next_type = parent_key_types[len(parent_key) - 1]
next_value = self._initialize_value(next_type)
elif isinstance(model, list):
# Handle lists
try:
idx = int(key)
except Exception as e:
raise IndexError(
f"Invalid list index '{key}' at '{path}': key = '{key}'; parent = '{parent}', parent_key = '{parent_key}'; model = '{model}'; {e}"
)
# Get next type from parent key type information
next_type = parent_key_types[len(parent_key) - 1]
if len(model) > idx:
next_value = model[idx]
else:
return
if is_final_key:
return
elif isinstance(model, dict):
# Handle dictionaries (auto-create missing keys)
# Get next type from parent key type information
next_type = parent_key_types[len(parent_key) - 1]
if is_final_key:
return
if key not in model:
return
else:
next_value = model[key]
else:
raise KeyError(f"Key '{key}' not found in model.")
# Move deeper
model = next_value
def get_nested_value(self, path: str) -> Any:
"""Retrieve a nested value from the model using a '/'-separated path.
Supports accessing nested attributes and list indices.
Args:
path (str): A '/'-separated path to the nested attribute (e.g., "key1/key2/0").
Returns:
Any: The retrieved value.
Raises:
KeyError: If a key is not found in the model.
IndexError: If a list index is out of bounds or invalid.
Example:
```python
class Address(PydanticBaseModel):
city: str
class User(PydanticBaseModel):
name: str
address: Address
user = User(name="Alice", address=Address(city="New York"))
city = user.get_nested_value("address/city")
print(city) # Output: "New York"
```
"""
path_elements = path.strip("/").split("/")
model: Any = self
for key in path_elements:
if isinstance(model, list):
try:
model = model[int(key)]
except (ValueError, IndexError) as e:
raise IndexError(f"Invalid list index at '{path}': {key}; {e}")
elif isinstance(model, dict):
try:
model = model[key]
except Exception as e:
raise KeyError(f"Invalid dict key at '{path}': {key}; {e}")
elif isinstance(model, BaseModel):
model = getattr(model, key)
else:
raise KeyError(f"Key '{key}' not found in model.")
return model
def set_nested_value(self, path: str, value: Any) -> None:
"""Set a nested value in the model using a '/'-separated path.
Supports modifying nested attributes and list indices while preserving Pydantic validation.
Automatically initializes missing `Optional`, `Union`, `dict`, and `list` fields if necessary.
If a missing field cannot be initialized, raises an exception.
Triggers the callbacks registered by track_nested_value().
Args:
path (str): A '/'-separated path to the nested attribute (e.g., "key1/key2/0").
value (Any): The new value to set.
Raises:
KeyError: If a key is not found in the model.
IndexError: If a list index is out of bounds or invalid.
ValueError: If a validation error occurs.
TypeError: If a missing field cannot be initialized.
Example:
```python
class Address(PydanticBaseModel):
city: Optional[str]
class User(PydanticBaseModel):
name: str
address: Optional[Address]
settings: Optional[Dict[str, Any]]
user = User(name="Alice", address=None, settings=None)
user.set_nested_value("address/city", "Los Angeles")
user.set_nested_value("settings/theme", "dark")
print(user.address.city) # Output: "Los Angeles"
print(user.settings) # Output: {'theme': 'dark'}
```
"""
path = path.strip("/")
# Store old value (if possible)
try:
old_value = self.get_nested_value(path)
except Exception as e:
# We can not get the old value
# raise ValueError(f"Can not get old (current) value of '{path}': {e}") from e
old_value = None
# Proceed with core logic
self._set_nested_value(path, value)
# Trigger all callbacks whose path is a prefix of set path
triggered = set()
nested_value_callbacks = get_private_attr(self, "nested_value_callbacks", dict())
for cb_path, callbacks in nested_value_callbacks.items():
# Match: cb_path == path, or cb_path is a prefix (parent) of path
pass
if path == cb_path or path.startswith(cb_path + "/"):
for cb in callbacks:
# Prevent duplicate calls
if (cb_path, id(cb)) not in triggered:
cb(self, path, old_value, value)
triggered.add((cb_path, id(cb)))
def _set_nested_value(self, path: str, value: Any) -> None:
"""Set a nested value core logic.
Args:
path (str): A '/'-separated path to the nested attribute (e.g., "key1/key2/0").
value (Any): The new value to set.
Raises:
KeyError: If a key is not found in the model.
IndexError: If a list index is out of bounds or invalid.
ValueError: If a validation error occurs.
TypeError: If a missing field cannot be initialized.
"""
path_elements = path.strip("/").split("/")
# The model we are currently working on
model: Any = self
# The model we get the type information from. It is a pydantic BaseModel
parent: BaseModel = model
# The field that provides type information for the current key
# Fields may have nested types that translates to a sequence of keys, not just one
# - my_field: Optional[list[OtherModel]] -> e.g. "myfield/0" for index 0
# parent_key = ["myfield",] ... ["myfield", "0"]
# parent_key_types = [list, OtherModel]
parent_key: list[str] = []
parent_key_types: list = []
for i, key in enumerate(path_elements):
is_final_key = i == len(path_elements) - 1
# Add current key to parent key to enable nested type tracking
parent_key.append(key)
# Get next value
next_value = None
if isinstance(model, BaseModel):
# Track parent and key for possible assignment later
parent = model
parent_key = [
key,
]
parent_key_types = self._get_key_types(model.__class__, key)
# If this is the final key, set the value
if is_final_key:
try:
model.__pydantic_validator__.validate_assignment(model, key, value)
except ValidationError as e:
raise ValueError(f"Error updating model: {e}") from e
return
# Attempt to access the next attribute, handling None values
next_value = getattr(model, key, None)
# Handle missing values (initialize dict/list/model if necessary)
if next_value is None:
next_type = parent_key_types[len(parent_key) - 1]
next_value = self._initialize_value(next_type)
if next_value is None:
raise TypeError(
f"Unable to initialize missing value for key '{key}' in path '{path}' with type {next_type} of {parent_key}:{parent_key_types}."
)
setattr(parent, key, next_value)
# pydantic may copy on validation assignment - reread to get the copied model
next_value = getattr(model, key, None)
elif isinstance(model, list):
# Handle lists (ensure index exists and modify safely)
try:
idx = int(key)
except Exception as e:
raise IndexError(
f"Invalid list index '{key}' at '{path}': key = '{key}'; parent = '{parent}', parent_key = '{parent_key}'; model = '{model}'; {e}"
)
# Get next type from parent key type information
next_type = parent_key_types[len(parent_key) - 1]
if len(model) > idx:
next_value = model[idx]
else:
# Extend the list with default values if index is out of range
while len(model) <= idx:
next_value = self._initialize_value(next_type)
if next_value is None:
raise TypeError(
f"Unable to initialize missing value for key '{key}' in path '{path}' with type {next_type} of {parent_key}:{parent_key_types}."
)
model.append(next_value)
if is_final_key:
if (
(isinstance(next_type, type) and not isinstance(value, next_type))
or (next_type is dict and not isinstance(value, dict))
or (next_type is list and not isinstance(value, list))
):
raise TypeError(
f"Expected type {next_type} for key '{key}' in path '{path}', but got {type(value)}: {value}"
)
model[idx] = value
return
elif isinstance(model, dict):
# Handle dictionaries (auto-create missing keys)
# Get next type from parent key type information
next_type = parent_key_types[len(parent_key) - 1]
if is_final_key:
if (
(isinstance(next_type, type) and not isinstance(value, next_type))
or (next_type is dict and not isinstance(value, dict))
or (next_type is list and not isinstance(value, list))
):
raise TypeError(
f"Expected type {next_type} for key '{key}' in path '{path}', but got {type(value)}: {value}"
)
model[key] = value
return
if key not in model:
next_value = self._initialize_value(next_type)
if next_value is None:
raise TypeError(
f"Unable to initialize missing value for key '{key}' in path '{path}' with type {next_type} of {parent_key}:{parent_key_types}."
)
model[key] = next_value
else:
next_value = model[key]
else:
raise KeyError(f"Key '{key}' not found in model.")
# Move deeper
model = next_value
@staticmethod
def _get_key_types(model: Type[BaseModel], key: str) -> List[Union[Type[Any], list, dict]]:
"""Returns a list of nested types for a given Pydantic model key.
- Skips `Optional` and `Union`, using only the first non-None type.
- Skips dictionary keys and only adds value types.
- Keeps `list` and `dict` as origins.
Args:
model (Type[BaseModel]): The Pydantic model class to inspect.
key (str): The attribute name in the model.
Returns:
List[Union[Type[Any], list, dict]]: A list of extracted types, preserving `list` and `dict` origins.
Raises:
TypeError: If the key does not exist or lacks a valid type annotation.
"""
if not inspect.isclass(model):
raise TypeError(f"Model '{model}' is not of class type.")
if key not in model.model_fields:
raise TypeError(f"Field '{key}' does not exist in model '{model.__name__}'.")
field_annotation = model.model_fields[key].annotation
if not field_annotation:
raise TypeError(
f"Missing type annotation for field '{key}' in model '{model.__name__}'."
)
nested_types: list[Union[Type[Any], list, dict]] = []
queue: list[Any] = [field_annotation]
while queue:
annotation = queue.pop(0)
origin = get_origin(annotation)
args = get_args(annotation)
# Handle Union (Optional[X] is treated as Union[X, None])
if origin is Union:
queue.extend(arg for arg in args if arg is not type(None))
continue
# Handle lists and dictionaries
if origin is list:
nested_types.append(list)
if args:
queue.append(args[0]) # Extract value type for list[T]
continue
if origin is dict:
nested_types.append(dict)
if len(args) == 2:
queue.append(args[1]) # Extract only the value type for dict[K, V]
continue
# If it's a BaseModel, add it to the list
if isinstance(annotation, type) and issubclass(annotation, BaseModel):
nested_types.append(annotation)
continue
# Otherwise, it's a standard type (e.g., str, int, bool, float, etc.)
nested_types.append(annotation)
return nested_types
@staticmethod
def _initialize_value(type_hint: Type[Any] | None | list[Any] | dict[Any, Any]) -> Any:
"""Initialize a missing value based on the provided type hint.
Args:
type_hint (Type[Any] | None | list[Any] | dict[Any, Any]): The type hint that determines
how the missing value should be initialized.
Returns:
Any: An instance of the expected type (e.g., list, dict, or Pydantic model), or `None`
if initialization is not possible.
Raises:
TypeError: If instantiation fails.
Example:
- For `list[str]`, returns `[]`
- For `dict[str, Any]`, returns `{}`
- For `Address` (a Pydantic model), returns a new `Address()` instance.
"""
if type_hint is None:
return None
# Handle direct instances of list or dict
if isinstance(type_hint, list):
return []
if isinstance(type_hint, dict):
return {}
origin = get_origin(type_hint)
# Handle generic list and dictionary
if origin is list:
return []
if origin is dict:
return {}
# Handle Pydantic models
if isinstance(type_hint, type) and issubclass(type_hint, BaseModel):
try:
return type_hint.model_construct()
except Exception as e:
raise TypeError(f"Failed to initialize model '{type_hint.__name__}': {e}")
# Handle standard built-in types (int, float, str, bool, etc.)
if isinstance(type_hint, type):
try:
return type_hint()
except Exception as e:
raise TypeError(f"Failed to initialize instance of '{type_hint.__name__}': {e}")
raise TypeError(f"Unsupported type hint '{type_hint}' for initialization.")
class PydanticBaseModel(PydanticModelNestedValueMixin, BaseModel):
"""Base model with pendulum datetime support, nested value utilities, and stable hashing.
This class provides:
- ISO 8601 serialization/deserialization of `pendulum.DateTime` fields.
- Nested attribute access and mutation via `PydanticModelNestedValueMixin`.
- A consistent hash using a UUID for use in sets and as dictionary keys
"""
# Enable custom serialization globally in config
@@ -160,6 +642,17 @@ class PydanticBaseModel(BaseModel):
validate_assignment=True,
)
_uuid: str = PrivateAttr(default_factory=lambda: str(uuid.uuid4()))
"""str: A private UUID string generated on instantiation, used for hashing."""
def __hash__(self) -> int:
"""Returns a stable hash based on the instance's UUID.
Returns:
int: Hash value derived from the model's UUID.
"""
return hash(self._uuid)
@field_validator("*", mode="before")
def validate_and_convert_pendulum(cls, value: Any, info: ValidationInfo) -> Any:
"""Validator to convert fields of type `pendulum.DateTime`.
@@ -188,8 +681,8 @@ class PydanticBaseModel(BaseModel):
if expected_type is pendulum.DateTime or expected_type is AwareDatetime:
try:
value = to_datetime(value)
except:
pass
except Exception as e:
raise ValueError(f"Cannot convert {value!r} to datetime: {e}")
return value
# Override Pydantics serialization for all DateTime fields
@@ -577,3 +1070,27 @@ class PydanticDateTimeSeries(PydanticBaseModel):
class ParametersBaseModel(PydanticBaseModel):
model_config = ConfigDict(extra="forbid")
def set_private_attr(
model: Union[PydanticBaseModel, PydanticModelNestedValueMixin], key: str, value: Any
) -> None:
"""Set a private attribute for a model instance (not stored in model itself)."""
if model not in _model_private_state:
_model_private_state[model] = {}
_model_private_state[model][key] = value
def get_private_attr(
model: Union[PydanticBaseModel, PydanticModelNestedValueMixin], key: str, default: Any = None
) -> Any:
"""Get a private attribute or return default."""
return _model_private_state.get(model, {}).get(key, default)
def del_private_attr(
model: Union[PydanticBaseModel, PydanticModelNestedValueMixin], key: str
) -> None:
"""Delete a private attribute."""
if model in _model_private_state and key in _model_private_state[model]:
del _model_private_state[model][key]

View File

@@ -3,7 +3,6 @@ from typing import Any, Optional
import numpy as np
from pydantic import Field, field_validator
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.devices.devicesabc import (
DeviceBase,
DeviceOptimizeResult,
@@ -11,8 +10,6 @@ from akkudoktoreos.devices.devicesabc import (
)
from akkudoktoreos.utils.utils import NumpyEncoder
logger = get_logger(__name__)
def max_charging_power_field(description: Optional[str] = None) -> float:
if description is None:
@@ -121,7 +118,8 @@ class Battery(DeviceBase):
def _setup(self) -> None:
"""Sets up the battery parameters based on configuration or provided parameters."""
assert self.parameters is not None
if self.parameters is None:
raise ValueError(f"Parameters not set: {self.parameters}")
self.capacity_wh = self.parameters.capacity_wh
self.initial_soc_percentage = self.parameters.initial_soc_percentage
self.charging_efficiency = self.parameters.charging_efficiency

View File

@@ -1,15 +1,12 @@
from typing import Optional
from akkudoktoreos.core.coreabc import SingletonMixin
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.devices.battery import Battery
from akkudoktoreos.devices.devicesabc import DevicesBase
from akkudoktoreos.devices.generic import HomeAppliance
from akkudoktoreos.devices.inverter import Inverter
from akkudoktoreos.devices.settings import DevicesCommonSettings
logger = get_logger(__name__)
class Devices(SingletonMixin, DevicesBase):
def __init__(self, settings: Optional[DevicesCommonSettings] = None):
@@ -39,10 +36,14 @@ class Devices(SingletonMixin, DevicesBase):
device.post_setup()
# Initialize the Devices simulation, it is a singleton.
devices = Devices()
# Initialize the Devices simulation, it is a singleton.
devices: Optional[Devices] = None
def get_devices() -> Devices:
global devices
# Fix circular import at runtime
if devices is None:
devices = Devices()
"""Gets the EOS Devices simulation."""
return devices

View File

@@ -3,6 +3,7 @@
from enum import Enum
from typing import Optional, Type
from loguru import logger
from pendulum import DateTime
from pydantic import Field, computed_field
@@ -12,12 +13,9 @@ from akkudoktoreos.core.coreabc import (
EnergyManagementSystemMixin,
PredictionMixin,
)
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.core.pydantic import ParametersBaseModel
from akkudoktoreos.utils.datetimeutil import to_duration
logger = get_logger(__name__)
class DeviceParameters(ParametersBaseModel):
device_id: str = Field(description="ID of device", examples="device1")
@@ -170,7 +168,8 @@ class DevicesBase(DevicesStartEndMixin, PredictionMixin):
def add_device(self, device: Optional["DeviceBase"]) -> None:
if device is None:
return
assert device.device_id not in self.devices, f"{device.device_id} already registered"
if device.device_id in self.devices:
raise ValueError(f"{device.device_id} already registered")
self.devices[device.device_id] = device
def remove_device(self, device: Type["DeviceBase"] | str) -> bool:

View File

@@ -3,11 +3,8 @@ from typing import Optional
import numpy as np
from pydantic import Field
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.devices.devicesabc import DeviceBase, DeviceParameters
logger = get_logger(__name__)
class HomeApplianceParameters(DeviceParameters):
"""Home Appliance Device Simulation Configuration."""
@@ -34,7 +31,8 @@ class HomeAppliance(DeviceBase):
super().__init__(parameters)
def _setup(self) -> None:
assert self.parameters is not None
if self.parameters is None:
raise ValueError(f"Parameters not set: {self.parameters}")
self.load_curve = np.zeros(self.hours) # Initialize the load curve with zeros
self.duration_h = self.parameters.duration_h
self.consumption_wh = self.parameters.consumption_wh

View File

@@ -1,6 +1,6 @@
from typing import List, Sequence
from akkudoktoreos.core.logging import get_logger
from loguru import logger
class Heatpump:
@@ -22,7 +22,6 @@ class Heatpump:
def __init__(self, max_heat_output: int, hours: int):
self.max_heat_output = max_heat_output
self.hours = hours
self.log = get_logger(__name__)
def __check_outside_temperature_range__(self, temp_celsius: float) -> bool:
"""Check if temperature is in valid range between -100 and 100 degree Celsius.
@@ -59,7 +58,7 @@ class Heatpump:
f"Outside temperature '{outside_temperature_celsius}' not in range "
"(min: -100 Celsius, max: 100 Celsius)"
)
self.log.error(err_msg)
logger.error(err_msg)
raise ValueError(err_msg)
def calculate_heating_output(self, outside_temperature_celsius: float) -> float:
@@ -87,7 +86,7 @@ class Heatpump:
f"Outside temperature '{outside_temperature_celsius}' not in range "
"(min: -100 Celsius, max: 100 Celsius)"
)
self.log.error(err_msg)
logger.error(err_msg)
raise ValueError(err_msg)
def calculate_heat_power(self, outside_temperature_celsius: float) -> float:
@@ -111,7 +110,7 @@ class Heatpump:
f"Outside temperature '{outside_temperature_celsius}' not in range "
"(min: -100 Celsius, max: 100 Celsius)"
)
self.log.error(err_msg)
logger.error(err_msg)
raise ValueError(err_msg)
def simulate_24h(self, temperatures: Sequence[float]) -> List[float]:

View File

@@ -1,13 +1,11 @@
from typing import Optional
from loguru import logger
from pydantic import Field
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.devices.devicesabc import DeviceBase, DeviceParameters
from akkudoktoreos.prediction.interpolator import get_eos_load_interpolator
logger = get_logger(__name__)
class InverterParameters(DeviceParameters):
"""Inverter Device Simulation Configuration."""
@@ -27,8 +25,25 @@ class Inverter(DeviceBase):
self.parameters: Optional[InverterParameters] = None
super().__init__(parameters)
self.scr_lookup: dict = {}
def _calculate_scr(self, consumption: float, generation: float) -> float:
"""Check if the consumption and production is in the lookup table. If not, calculate and store the value."""
if consumption not in self.scr_lookup:
self.scr_lookup[consumption] = {}
if generation not in self.scr_lookup[consumption]:
scr = self.self_consumption_predictor.calculate_self_consumption(
consumption, generation
)
self.scr_lookup[consumption][generation] = scr
return scr
return self.scr_lookup[consumption][generation]
def _setup(self) -> None:
assert self.parameters is not None
if self.parameters is None:
raise ValueError(f"Parameters not set: {self.parameters}")
if self.parameters.battery_id is None:
# For the moment raise exception
# TODO: Make battery configurable by config
@@ -41,7 +56,8 @@ class Inverter(DeviceBase):
) # Maximum power that the inverter can handle
def _post_setup(self) -> None:
assert self.parameters is not None
if self.parameters is None:
raise ValueError(f"Parameters not set: {self.parameters}")
self.battery = self.devices.get_device_by_id(self.parameters.battery_id)
def process_energy(
@@ -60,9 +76,8 @@ class Inverter(DeviceBase):
grid_import = -remaining_power # Negative indicates feeding into the grid
self_consumption = self.max_power_wh
else:
scr = self.self_consumption_predictor.calculate_self_consumption(
consumption, generation
)
# Calculate scr with lookup table
scr = self._calculate_scr(consumption, generation)
# Remaining power after consumption
remaining_power = (generation - consumption) * scr # EVQ

View File

@@ -3,13 +3,10 @@ from typing import Optional
from pydantic import Field
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.devices.battery import BaseBatteryParameters
from akkudoktoreos.devices.generic import HomeApplianceParameters
from akkudoktoreos.devices.inverter import InverterParameters
logger = get_logger(__name__)
class DevicesCommonSettings(SettingsBaseModel):
"""Base configuration for devices simulation settings."""

View File

@@ -9,6 +9,7 @@ The measurements can be added programmatically or imported from a file or JSON s
from typing import Any, ClassVar, List, Optional
import numpy as np
from loguru import logger
from numpydantic import NDArray, Shape
from pendulum import DateTime, Duration
from pydantic import Field, computed_field
@@ -16,11 +17,8 @@ from pydantic import Field, computed_field
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.coreabc import SingletonMixin
from akkudoktoreos.core.dataabc import DataImportMixin, DataRecord, DataSequence
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.utils.datetimeutil import to_duration
logger = get_logger(__name__)
class MeasurementCommonSettings(SettingsBaseModel):
"""Measurement Configuration."""

View File

@@ -4,6 +4,7 @@ from typing import Any, Optional
import numpy as np
from deap import algorithms, base, creator, tools
from loguru import logger
from pydantic import Field, field_validator, model_validator
from typing_extensions import Self
@@ -13,7 +14,6 @@ from akkudoktoreos.core.coreabc import (
EnergyManagementSystemMixin,
)
from akkudoktoreos.core.ems import EnergyManagementParameters, SimulationResult
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.core.pydantic import ParametersBaseModel
from akkudoktoreos.devices.battery import (
Battery,
@@ -25,8 +25,6 @@ from akkudoktoreos.devices.generic import HomeAppliance, HomeApplianceParameters
from akkudoktoreos.devices.inverter import Inverter, InverterParameters
from akkudoktoreos.utils.utils import NumpyEncoder
logger = get_logger(__name__)
class OptimizationParameters(ParametersBaseModel):
ems: EnergyManagementParameters
@@ -121,7 +119,7 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
if self.fix_seed is not None:
random.seed(self.fix_seed)
elif logger.level == "DEBUG":
self.fix_seed = random.randint(1, 100000000000)
self.fix_seed = random.randint(1, 100000000000) # noqa: S311
random.seed(self.fix_seed)
def decode_charge_discharge(

View File

@@ -3,9 +3,6 @@ from typing import List, Optional
from pydantic import Field
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.logging import get_logger
logger = get_logger(__name__)
class OptimizationCommonSettings(SettingsBaseModel):

View File

@@ -3,11 +3,8 @@
from pydantic import ConfigDict
from akkudoktoreos.core.coreabc import ConfigMixin, PredictionMixin
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.core.pydantic import PydanticBaseModel
logger = get_logger(__name__)
class OptimizationBase(ConfigMixin, PredictionMixin, PydanticBaseModel):
"""Base class for handling optimization data.

View File

@@ -1,9 +1,20 @@
from typing import Optional
from pydantic import Field
from pydantic import Field, field_validator
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.prediction.elecpriceabc import ElecPriceProvider
from akkudoktoreos.prediction.elecpriceimport import ElecPriceImportCommonSettings
from akkudoktoreos.prediction.prediction import get_prediction
prediction_eos = get_prediction()
# Valid elecprice providers
elecprice_providers = [
provider.provider_id()
for provider in prediction_eos.providers
if isinstance(provider, ElecPriceProvider)
]
class ElecPriceCommonSettings(SettingsBaseModel):
@@ -17,7 +28,23 @@ class ElecPriceCommonSettings(SettingsBaseModel):
charges_kwh: Optional[float] = Field(
default=None, ge=0, description="Electricity price charges (€/kWh).", examples=[0.21]
)
vat_rate: Optional[float] = Field(
default=1.19,
ge=0,
description="VAT rate factor applied to electricity price when charges are used.",
examples=[1.19],
)
provider_settings: Optional[ElecPriceImportCommonSettings] = Field(
default=None, description="Provider settings", examples=[None]
)
# Validators
@field_validator("provider", mode="after")
@classmethod
def validate_provider(cls, value: Optional[str]) -> Optional[str]:
if value is None or value in elecprice_providers:
return value
raise ValueError(
f"Provider '{value}' is not a valid electricity price provider: {elecprice_providers}."
)

View File

@@ -9,11 +9,8 @@ from typing import List, Optional
from pydantic import Field, computed_field
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.prediction.predictionabc import PredictionProvider, PredictionRecord
logger = get_logger(__name__)
class ElecPriceDataRecord(PredictionRecord):
"""Represents a electricity price data record containing various price attributes at a specific datetime.

View File

@@ -11,17 +11,15 @@ from typing import Any, List, Optional, Union
import numpy as np
import pandas as pd
import requests
from loguru import logger
from pydantic import ValidationError
from statsmodels.tsa.holtwinters import ExponentialSmoothing
from akkudoktoreos.core.cache import cache_in_file
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.core.pydantic import PydanticBaseModel
from akkudoktoreos.prediction.elecpriceabc import ElecPriceProvider
from akkudoktoreos.utils.datetimeutil import to_datetime, to_duration
logger = get_logger(__name__)
class AkkudoktorElecPriceMeta(PydanticBaseModel):
start_timestamp: str
@@ -104,12 +102,13 @@ class ElecPriceAkkudoktor(ElecPriceProvider):
- add the file cache again.
"""
source = "https://api.akkudoktor.net"
assert self.start_datetime # mypy fix
if not self.start_datetime:
raise ValueError(f"Start DateTime not set: {self.start_datetime}")
# Try to take data from 5 weeks back for prediction
date = to_datetime(self.start_datetime - to_duration("35 days"), as_string="YYYY-MM-DD")
last_date = to_datetime(self.end_datetime, as_string="YYYY-MM-DD")
url = f"{source}/prices?start={date}&end={last_date}&tz={self.config.general.timezone}"
response = requests.get(url)
response = requests.get(url, timeout=10)
logger.debug(f"Response from {url}: {response}")
response.raise_for_status() # Raise an error for bad responses
akkudoktor_data = self._validate_data(response.content)
@@ -148,7 +147,8 @@ class ElecPriceAkkudoktor(ElecPriceProvider):
"""
# Get Akkudoktor electricity price data
akkudoktor_data = self._request_forecast(force_update=force_update) # type: ignore
assert self.start_datetime # mypy fix
if not self.start_datetime:
raise ValueError(f"Start DateTime not set: {self.start_datetime}")
# Assumption that all lists are the same length and are ordered chronologically
# in ascending order and have the same timestamps.
@@ -178,7 +178,10 @@ class ElecPriceAkkudoktor(ElecPriceProvider):
)
amount_datasets = len(self.records)
assert highest_orig_datetime # mypy fix
if not highest_orig_datetime: # mypy fix
error_msg = f"Highest original datetime not available: {highest_orig_datetime}"
logger.error(error_msg)
raise ValueError(error_msg)
# 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_hours = int(

View File

@@ -0,0 +1,257 @@
"""Retrieves and processes electricity price forecast data from Energy-Charts.
This module provides classes and mappings to manage electricity price data obtained from the
Energy-Charts API, including support for various electricity price attributes such as temperature,
humidity, cloud cover, and solar irradiance. The data is mapped to the `ElecPriceDataRecord`
format, enabling consistent access to forecasted and historical electricity price attributes.
"""
from datetime import datetime
from typing import Any, List, Optional, Union
import numpy as np
import pandas as pd
import requests
from loguru import logger
from pydantic import ValidationError
from statsmodels.tsa.holtwinters import ExponentialSmoothing
from akkudoktoreos.core.cache import cache_in_file
from akkudoktoreos.core.pydantic import PydanticBaseModel
from akkudoktoreos.prediction.elecpriceabc import ElecPriceProvider
from akkudoktoreos.utils.datetimeutil import to_datetime, to_duration
class EnergyChartsElecPrice(PydanticBaseModel):
license_info: str
unix_seconds: List[int]
price: List[float]
unit: str
deprecated: bool
class ElecPriceEnergyCharts(ElecPriceProvider):
"""Fetch and process electricity price forecast data from Energy-Charts.
ElecPriceEnergyCharts is a singleton-based class that retrieves electricity price forecast data
from the Energy-Charts API and maps it to `ElecPriceDataRecord` fields, applying
any necessary scaling or unit corrections. It manages the forecast over a range
of hours into the future and retains historical data.
Attributes:
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 `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.
_request_forecast(): Fetches the forecast from the Energy-Charts API.
_update_data(): Processes and updates forecast data from Energy-Charts in ElecPriceDataRecord format.
"""
highest_orig_datetime: Optional[datetime] = None
@classmethod
def provider_id(cls) -> str:
"""Return the unique identifier for the Energy-Charts provider."""
return "ElecPriceEnergyCharts"
@classmethod
def _validate_data(cls, json_str: Union[bytes, Any]) -> EnergyChartsElecPrice:
"""Validate Energy-Charts Electricity Price forecast data."""
try:
energy_charts_data = EnergyChartsElecPrice.model_validate_json(json_str)
except ValidationError as e:
error_msg = ""
for error in e.errors():
field = " -> ".join(str(x) for x in error["loc"])
message = error["msg"]
error_type = error["type"]
error_msg += f"Field: {field}\nError: {message}\nType: {error_type}\n"
logger.error(f"Energy-Charts schema change: {error_msg}")
raise ValueError(error_msg)
return energy_charts_data
@cache_in_file(with_ttl="1 hour")
def _request_forecast(self, start_date: Optional[str] = None) -> EnergyChartsElecPrice:
"""Fetch electricity price forecast data from Energy-Charts API.
This method sends a request to Energy-Charts API to retrieve forecast data for a specified
date range. The response data is parsed and returned as JSON for further processing.
Returns:
dict: The parsed JSON response from Energy-Charts API containing forecast data.
Raises:
ValueError: If the API response does not include expected `electricity price` data.
"""
source = "https://api.energy-charts.info"
if start_date is None:
# Try to take data from 5 weeks back for prediction
start_date = to_datetime(
self.start_datetime - to_duration("35 days"), as_string="YYYY-MM-DD"
)
last_date = to_datetime(self.end_datetime, as_string="YYYY-MM-DD")
url = f"{source}/price?bzn=DE-LU&start={start_date}&end={last_date}"
response = requests.get(url, timeout=30)
logger.debug(f"Response from {url}: {response}")
response.raise_for_status() # Raise an error for bad responses
energy_charts_data = self._validate_data(response.content)
# We are working on fresh data (no cache), report update time
self.update_datetime = to_datetime(in_timezone=self.config.general.timezone)
return energy_charts_data
def _parse_data(self, energy_charts_data: EnergyChartsElecPrice) -> pd.Series:
# Assumption that all lists are the same length and are ordered chronologically
# in ascending order and have the same timestamps.
# Get charges_kwh in wh
charges_wh = (self.config.elecprice.charges_kwh or 0) / 1000
# Initialize
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
# Iterate over timestamps and prices together
for unix_sec, price_eur_per_mwh in zip(
energy_charts_data.unix_seconds, energy_charts_data.price
):
orig_datetime = to_datetime(unix_sec, in_timezone=self.config.general.timezone)
# Track the latest datetime
if highest_orig_datetime is None or orig_datetime > highest_orig_datetime:
highest_orig_datetime = orig_datetime
# Convert EUR/MWh to EUR/Wh, apply charges and VAT if charges > 0
if charges_wh > 0:
vat_rate = self.config.elecprice.vat_rate or 1.19
price_wh = ((price_eur_per_mwh / 1_000_000) + charges_wh) * vat_rate
else:
price_wh = price_eur_per_mwh / 1_000_000
# Store in series
series_data.at[orig_datetime] = price_wh
return series_data
def _cap_outliers(self, data: np.ndarray, sigma: int = 2) -> np.ndarray:
mean = data.mean()
std = data.std()
lower_bound = mean - sigma * std
upper_bound = mean + sigma * std
capped_data = data.clip(min=lower_bound, max=upper_bound)
return capped_data
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(hours)
def _predict_median(self, history: np.ndarray, hours: int) -> np.ndarray:
clean_history = self._cap_outliers(history)
return np.full(hours, np.median(clean_history))
def _update_data(
self, force_update: Optional[bool] = False
) -> None: # tuple[np.ndarray, np.ndarray, np.ndarray]:
"""Update forecast data in the ElecPriceDataRecord format.
Retrieves data from Energy-Charts, maps each Energy-Charts field to the corresponding
`ElecPriceDataRecord` and applies any necessary scaling.
The final mapped and processed data is inserted into the sequence as `ElecPriceDataRecord`.
"""
# New prices are available every day at 14:00
now = pd.Timestamp.now(tz=self.config.general.timezone)
midnight = now.normalize()
hours_ahead = 23 if now.time() < pd.Timestamp("14:00").time() else 47
end = midnight + pd.Timedelta(hours=hours_ahead)
if not self.start_datetime:
raise ValueError(f"Start DateTime not set: {self.start_datetime}")
# Determine if update is needed and how many days
past_days = 35
if self.highest_orig_datetime:
history_series = self.key_to_series(
key="elecprice_marketprice_wh", start_datetime=self.start_datetime
)
# If history lower, then start_datetime
if history_series.index.min() <= self.start_datetime:
past_days = 0
needs_update = end > self.highest_orig_datetime
else:
needs_update = True
if needs_update:
logger.info(
f"Update ElecPriceEnergyCharts is needed, last in history: {self.highest_orig_datetime}"
)
# Set start_date try to take data from 5 weeks back for prediction
start_date = to_datetime(
self.start_datetime - to_duration(f"{past_days} days"), as_string="YYYY-MM-DD"
)
# Get Energy-Charts electricity price data
energy_charts_data = self._request_forecast(
start_date=start_date, force_update=force_update
) # type: ignore
# Parse and store data
series_data = self._parse_data(energy_charts_data)
self.highest_orig_datetime = series_data.index.max()
self.key_from_series("elecprice_marketprice_wh", series_data)
else:
logger.info(
f"No Update ElecPriceEnergyCharts is needed, last in history: {self.highest_orig_datetime}"
)
# Generate history array for prediction
history = self.key_to_array(
key="elecprice_marketprice_wh",
end_datetime=self.highest_orig_datetime,
fill_method="linear",
)
amount_datasets = len(self.records)
if not self.highest_orig_datetime: # mypy fix
error_msg = f"Highest original datetime not available: {self.highest_orig_datetime}"
logger.error(error_msg)
raise ValueError(error_msg)
# 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_hours = int(
self.config.prediction.hours
- ((self.highest_orig_datetime - self.start_datetime).total_seconds() // 3600)
)
if needed_hours <= 0:
logger.warning(
f"No prediction needed. needed_hours={needed_hours}, hours={self.config.prediction.hours},highest_orig_datetime {self.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, 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, hours=needed_hours)
elif amount_datasets > 0: # not enough data for ets, do median
prediction = self._predict_median(history, hours=needed_hours)
else:
logger.error("No data available for prediction")
raise ValueError("No data available")
# write predictions into the records, update if exist.
prediction_series = pd.Series(
data=prediction,
index=[
self.highest_orig_datetime + to_duration(f"{i + 1} hours")
for i in range(len(prediction))
],
)
self.key_from_series("elecprice_marketprice_wh", prediction_series)

View File

@@ -9,15 +9,13 @@ format, enabling consistent access to forecasted and historical elecprice attrib
from pathlib import Path
from typing import Optional, Union
from loguru import logger
from pydantic import Field, field_validator
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.prediction.elecpriceabc import ElecPriceProvider
from akkudoktoreos.prediction.predictionabc import PredictionImportProvider
logger = get_logger(__name__)
class ElecPriceImportCommonSettings(SettingsBaseModel):
"""Common settings for elecprice data import from file or JSON String."""

View File

@@ -14,7 +14,7 @@ class SelfConsumptionProbabilityInterpolator:
self.filepath = filepath
# Load the RegularGridInterpolator
with open(self.filepath, "rb") as file:
self.interpolator: RegularGridInterpolator = pickle.load(file)
self.interpolator: RegularGridInterpolator = pickle.load(file) # noqa: S301
@lru_cache(maxsize=128)
def generate_points(

View File

@@ -2,14 +2,23 @@
from typing import Optional, Union
from pydantic import Field
from pydantic import Field, field_validator
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.prediction.loadabc import LoadProvider
from akkudoktoreos.prediction.loadakkudoktor import LoadAkkudoktorCommonSettings
from akkudoktoreos.prediction.loadimport import LoadImportCommonSettings
from akkudoktoreos.prediction.loadvrm import LoadVrmCommonSettings
from akkudoktoreos.prediction.prediction import get_prediction
logger = get_logger(__name__)
prediction_eos = get_prediction()
# Valid load providers
load_providers = [
provider.provider_id()
for provider in prediction_eos.providers
if isinstance(provider, LoadProvider)
]
class LoadCommonSettings(SettingsBaseModel):
@@ -21,6 +30,14 @@ class LoadCommonSettings(SettingsBaseModel):
examples=["LoadAkkudoktor"],
)
provider_settings: Optional[Union[LoadAkkudoktorCommonSettings, LoadImportCommonSettings]] = (
Field(default=None, description="Provider settings", examples=[None])
)
provider_settings: Optional[
Union[LoadAkkudoktorCommonSettings, LoadVrmCommonSettings, LoadImportCommonSettings]
] = Field(default=None, description="Provider settings", examples=[None])
# Validators
@field_validator("provider", mode="after")
@classmethod
def validate_provider(cls, value: Optional[str]) -> Optional[str]:
if value is None or value in load_providers:
return value
raise ValueError(f"Provider '{value}' is not a valid load provider: {load_providers}.")

View File

@@ -9,11 +9,8 @@ from typing import List, Optional
from pydantic import Field
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.prediction.predictionabc import PredictionProvider, PredictionRecord
logger = get_logger(__name__)
class LoadDataRecord(PredictionRecord):
"""Represents a load data record containing various load attributes at a specific datetime."""

View File

@@ -3,15 +3,13 @@
from typing import Optional
import numpy as np
from loguru import logger
from pydantic import Field
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.prediction.loadabc import LoadProvider
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime, to_duration
logger = get_logger(__name__)
class LoadAkkudoktorCommonSettings(SettingsBaseModel):
"""Common settings for load data import from file."""
@@ -122,10 +120,11 @@ class LoadAkkudoktor(LoadProvider):
}
if date.day_of_week < 5:
# Monday to Friday (0..4)
values["load_mean_adjusted"] = hourly_stats[0] + weekday_adjust[date.hour]
value_adjusted = hourly_stats[0] + weekday_adjust[date.hour]
else:
# Saturday, Sunday (5, 6)
values["load_mean_adjusted"] = hourly_stats[0] + weekend_adjust[date.hour]
value_adjusted = hourly_stats[0] + weekend_adjust[date.hour]
values["load_mean_adjusted"] = max(0, value_adjusted)
self.update_value(date, values)
date += to_duration("1 hour")
# We are working on fresh data (no cache), report update time

View File

@@ -9,15 +9,13 @@ format, enabling consistent access to forecasted and historical load attributes.
from pathlib import Path
from typing import Optional, Union
from loguru import logger
from pydantic import Field, field_validator
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.prediction.loadabc import LoadProvider
from akkudoktoreos.prediction.predictionabc import PredictionImportProvider
logger = get_logger(__name__)
class LoadImportCommonSettings(SettingsBaseModel):
"""Common settings for load data import from file or JSON string."""

View File

@@ -0,0 +1,109 @@
"""Retrieves load forecast data from VRM API."""
from typing import Any, Optional, Union
import requests
from loguru import logger
from pendulum import DateTime
from pydantic import Field, ValidationError
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.pydantic import PydanticBaseModel
from akkudoktoreos.prediction.loadabc import LoadProvider
from akkudoktoreos.utils.datetimeutil import to_datetime
class VrmForecastRecords(PydanticBaseModel):
vrm_consumption_fc: list[tuple[int, float]]
solar_yield_forecast: list[tuple[int, float]]
class VrmForecastResponse(PydanticBaseModel):
success: bool
records: VrmForecastRecords
totals: dict
class LoadVrmCommonSettings(SettingsBaseModel):
"""Common settings for VRM API."""
load_vrm_token: str = Field(
default="your-token", description="Token for Connecting VRM API", examples=["your-token"]
)
load_vrm_idsite: int = Field(default=12345, description="VRM-Installation-ID", examples=[12345])
class LoadVrm(LoadProvider):
"""Fetch Load forecast data from VRM API."""
@classmethod
def provider_id(cls) -> str:
return "LoadVrm"
@classmethod
def _validate_data(cls, json_str: Union[bytes, Any]) -> VrmForecastResponse:
"""Validate the VRM API load forecast response."""
try:
return VrmForecastResponse.model_validate_json(json_str)
except ValidationError as e:
error_msg = "\n".join(
f"Field: {' -> '.join(str(x) for x in err['loc'])}\n"
f"Error: {err['msg']}\nType: {err['type']}"
for err in e.errors()
)
logger.error(f"VRM-API schema validation failed:\n{error_msg}")
raise ValueError(error_msg)
def _request_forecast(self, start_ts: int, end_ts: int) -> VrmForecastResponse:
"""Fetch forecast data from Victron VRM API."""
base_url = "https://vrmapi.victronenergy.com/v2/installations"
installation_id = self.config.load.provider_settings.load_vrm_idsite
api_token = self.config.load.provider_settings.load_vrm_token
url = f"{base_url}/{installation_id}/stats?type=forecast&start={start_ts}&end={end_ts}&interval=hours"
headers = {"X-Authorization": f"Token {api_token}", "Content-Type": "application/json"}
logger.debug(f"Requesting VRM load forecast: {url}")
try:
response = requests.get(url, headers=headers, timeout=30)
response.raise_for_status()
except requests.RequestException as e:
logger.error(f"Error during VRM API request: {e}")
raise RuntimeError("Failed to fetch load forecast from VRM API") from e
self.update_datetime = to_datetime(in_timezone=self.config.general.timezone)
return self._validate_data(response.content)
def _ts_to_datetime(self, timestamp: int) -> DateTime:
"""Convert UNIX ms timestamp to timezone-aware datetime."""
return to_datetime(timestamp / 1000, in_timezone=self.config.general.timezone)
def _update_data(self, force_update: Optional[bool] = False) -> None:
"""Fetch and store VRM load forecast as load_mean and related values."""
start_date = self.start_datetime.start_of("day")
end_date = self.start_datetime.add(hours=self.config.prediction.hours)
start_ts = int(start_date.timestamp())
end_ts = int(end_date.timestamp())
logger.info(f"Updating Load forecast from VRM: {start_date} to {end_date}")
vrm_forecast_data = self._request_forecast(start_ts, end_ts)
load_mean_data = []
for timestamp, value in vrm_forecast_data.records.vrm_consumption_fc:
date = self._ts_to_datetime(timestamp)
rounded_value = round(value, 2)
self.update_value(
date,
{"load_mean": rounded_value, "load_std": 0.0, "load_mean_adjusted": rounded_value},
)
load_mean_data.append((date, rounded_value))
logger.debug(f"Updated load_mean with {len(load_mean_data)} entries.")
self.update_datetime = to_datetime(in_timezone=self.config.general.timezone)
if __name__ == "__main__":
lv = LoadVrm()
lv._update_data()

View File

@@ -32,12 +32,15 @@ from pydantic import Field
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.prediction.elecpriceakkudoktor import ElecPriceAkkudoktor
from akkudoktoreos.prediction.elecpriceenergycharts import ElecPriceEnergyCharts
from akkudoktoreos.prediction.elecpriceimport import ElecPriceImport
from akkudoktoreos.prediction.loadakkudoktor import LoadAkkudoktor
from akkudoktoreos.prediction.loadimport import LoadImport
from akkudoktoreos.prediction.loadvrm import LoadVrm
from akkudoktoreos.prediction.predictionabc import PredictionContainer
from akkudoktoreos.prediction.pvforecastakkudoktor import PVForecastAkkudoktor
from akkudoktoreos.prediction.pvforecastimport import PVForecastImport
from akkudoktoreos.prediction.pvforecastvrm import PVForecastVrm
from akkudoktoreos.prediction.weatherbrightsky import WeatherBrightSky
from akkudoktoreos.prediction.weatherclearoutside import WeatherClearOutside
from akkudoktoreos.prediction.weatherimport import WeatherImport
@@ -83,10 +86,13 @@ class Prediction(PredictionContainer):
providers: List[
Union[
ElecPriceAkkudoktor,
ElecPriceEnergyCharts,
ElecPriceImport,
LoadAkkudoktor,
LoadVrm,
LoadImport,
PVForecastAkkudoktor,
PVForecastVrm,
PVForecastImport,
WeatherBrightSky,
WeatherClearOutside,
@@ -97,10 +103,13 @@ class Prediction(PredictionContainer):
# Initialize forecast providers, all are singletons.
elecprice_akkudoktor = ElecPriceAkkudoktor()
elecprice_energy_charts = ElecPriceEnergyCharts()
elecprice_import = ElecPriceImport()
load_akkudoktor = LoadAkkudoktor()
load_vrm = LoadVrm()
load_import = LoadImport()
pvforecast_akkudoktor = PVForecastAkkudoktor()
pvforecast_vrm = PVForecastVrm()
pvforecast_import = PVForecastImport()
weather_brightsky = WeatherBrightSky()
weather_clearoutside = WeatherClearOutside()
@@ -114,10 +123,13 @@ def get_prediction() -> Prediction:
prediction = Prediction(
providers=[
elecprice_akkudoktor,
elecprice_energy_charts,
elecprice_import,
load_akkudoktor,
load_vrm,
load_import,
pvforecast_akkudoktor,
pvforecast_vrm,
pvforecast_import,
weather_brightsky,
weather_clearoutside,

View File

@@ -10,6 +10,7 @@ and manipulation of configuration and prediction data in a clear, scalable, and
from typing import List, Optional
from loguru import logger
from pendulum import DateTime
from pydantic import Field, computed_field
@@ -22,11 +23,8 @@ from akkudoktoreos.core.dataabc import (
DataRecord,
DataSequence,
)
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.utils.datetimeutil import to_duration
logger = get_logger(__name__)
class PredictionBase(DataBase, MeasurementMixin):
"""Base class for handling prediction data.

View File

@@ -1,15 +1,24 @@
"""PV forecast module for PV power predictions."""
from typing import Any, ClassVar, List, Optional, Self
from typing import Any, List, Optional, Self, Union
from pydantic import Field, computed_field, field_validator, model_validator
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.prediction.prediction import get_prediction
from akkudoktoreos.prediction.pvforecastabc import PVForecastProvider
from akkudoktoreos.prediction.pvforecastimport import PVForecastImportCommonSettings
from akkudoktoreos.prediction.pvforecastvrm import PVforecastVrmCommonSettings
from akkudoktoreos.utils.docs import get_model_structure_from_examples
logger = get_logger(__name__)
prediction_eos = get_prediction()
# Valid PV forecast providers
pvforecast_providers = [
provider.provider_id()
for provider in prediction_eos.providers
if isinstance(provider, PVForecastProvider)
]
class PVForecastPlaneSetting(SettingsBaseModel):
@@ -17,14 +26,18 @@ class PVForecastPlaneSetting(SettingsBaseModel):
# latitude: Optional[float] = Field(default=None, description="Latitude in decimal degrees, between -90 and 90, north is positive (ISO 19115) (°)")
surface_tilt: Optional[float] = Field(
default=None,
default=30.0,
ge=0.0,
le=90.0,
description="Tilt angle from horizontal plane. Ignored for two-axis tracking.",
examples=[10.0, 20.0],
)
surface_azimuth: Optional[float] = Field(
default=None,
default=180.0,
ge=0.0,
le=360.0,
description="Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).",
examples=[10.0, 20.0],
examples=[180.0, 90.0],
)
userhorizon: Optional[List[float]] = Field(
default=None,
@@ -71,7 +84,7 @@ class PVForecastPlaneSetting(SettingsBaseModel):
default=None, description="Model of the inverter of this plane.", examples=[None]
)
inverter_paco: Optional[int] = Field(
default=None, description="AC power rating of the inverter. [W]", examples=[6000, 4000]
default=None, description="AC power rating of the inverter [W].", examples=[6000, 4000]
)
modules_per_string: Optional[int] = Field(
default=None,
@@ -122,26 +135,32 @@ class PVForecastCommonSettings(SettingsBaseModel):
examples=["PVForecastAkkudoktor"],
)
provider_settings: Optional[
Union[PVForecastImportCommonSettings, PVforecastVrmCommonSettings]
] = Field(default=None, description="Provider settings", examples=[None])
planes: Optional[list[PVForecastPlaneSetting]] = Field(
default=None,
description="Plane configuration.",
examples=[get_model_structure_from_examples(PVForecastPlaneSetting, True)],
)
max_planes: ClassVar[int] = 6 # Maximum number of planes that can be set
@field_validator("planes")
def validate_planes(
cls, planes: Optional[list[PVForecastPlaneSetting]]
) -> Optional[list[PVForecastPlaneSetting]]:
if planes is not None and len(planes) > cls.max_planes:
raise ValueError(f"Maximum number of supported planes: {cls.max_planes}.")
return planes
provider_settings: Optional[PVForecastImportCommonSettings] = Field(
default=None, description="Provider settings", examples=[None]
max_planes: Optional[int] = Field(
default=0,
ge=0,
description="Maximum number of planes that can be set",
)
# Validators
@field_validator("provider", mode="after")
@classmethod
def validate_provider(cls, value: Optional[str]) -> Optional[str]:
if value is None or value in pvforecast_providers:
return value
raise ValueError(
f"Provider '{value}' is not a valid PV forecast provider: {pvforecast_providers}."
)
## Computed fields
@computed_field # type: ignore[prop-decorator]
@property

View File

@@ -7,13 +7,11 @@ Notes:
from abc import abstractmethod
from typing import List, Optional
from loguru import logger
from pydantic import Field
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.prediction.predictionabc import PredictionProvider, PredictionRecord
logger = get_logger(__name__)
class PVForecastDataRecord(PredictionRecord):
"""Represents a pvforecast data record containing various pvforecast attributes at a specific datetime."""

View File

@@ -27,14 +27,14 @@ Example:
"planes": [
{
"peakpower": 5.0,
"surface_azimuth": -10,
"surface_azimuth": 170,
"surface_tilt": 7,
"userhorizon": [20, 27, 22, 20],
"inverter_paco": 10000,
},
{
"peakpower": 4.8,
"surface_azimuth": -90,
"surface_azimuth": 90,
"surface_tilt": 7,
"userhorizon": [30, 30, 30, 50],
"inverter_paco": 10000,
@@ -78,10 +78,10 @@ Methods:
from typing import Any, List, Optional, Union
import requests
from pydantic import Field, ValidationError, computed_field
from loguru import logger
from pydantic import Field, ValidationError, computed_field, field_validator
from akkudoktoreos.core.cache import cache_in_file
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.core.pydantic import PydanticBaseModel
from akkudoktoreos.prediction.pvforecastabc import (
PVForecastDataRecord,
@@ -89,13 +89,11 @@ from akkudoktoreos.prediction.pvforecastabc import (
)
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime
logger = get_logger(__name__)
class AkkudoktorForecastHorizon(PydanticBaseModel):
altitude: int
azimuthFrom: int
azimuthTo: int
azimuthFrom: float
azimuthTo: float
class AkkudoktorForecastMeta(PydanticBaseModel):
@@ -114,6 +112,30 @@ class AkkudoktorForecastMeta(PydanticBaseModel):
horizont: List[List[AkkudoktorForecastHorizon]]
horizontString: List[str]
@field_validator("power", "azimuth", "tilt", "powerInverter", mode="before")
@classmethod
def ensure_list(cls, v: Any) -> List[int]:
return v if isinstance(v, list) else [v]
@field_validator("horizont", mode="before")
@classmethod
def normalize_horizont(cls, v: Any) -> List[List[AkkudoktorForecastHorizon]]:
if isinstance(v, list):
# Case: flat list of dicts
if v and isinstance(v[0], dict):
return [v]
# Already in correct nested form
if v and isinstance(v[0], list):
return v
return v
@field_validator("horizontString", mode="before")
@classmethod
def parse_horizont_string(cls, v: Any) -> List[str]:
if isinstance(v, str):
return [s.strip() for s in v.split(",")]
return v
class AkkudoktorForecastValue(PydanticBaseModel):
datetime: str
@@ -221,13 +243,18 @@ class PVForecastAkkudoktor(PVForecastProvider):
for i in range(len(self.config.pvforecast.planes)):
query_params.append(f"power={int(self.config.pvforecast.planes_peakpower[i] * 1000)}")
query_params.append(f"azimuth={int(self.config.pvforecast.planes_azimuth[i])}")
# EOS orientation of of pv modules in azimuth in degree:
# north=0, east=90, south=180, west=270
# Akkudoktor orientation of pv modules in azimuth in degree:
# north=+-180, east=-90, south=0, west=90
azimuth_akkudoktor = int(self.config.pvforecast.planes_azimuth[i]) - 180
query_params.append(f"azimuth={azimuth_akkudoktor}")
query_params.append(f"tilt={int(self.config.pvforecast.planes_tilt[i])}")
query_params.append(
f"powerInverter={int(self.config.pvforecast.planes_inverter_paco[i])}"
)
horizon_values = ",".join(
str(int(h)) for h in self.config.pvforecast.planes_userhorizon[i]
str(round(h)) for h in self.config.pvforecast.planes_userhorizon[i]
)
query_params.append(f"horizont={horizon_values}")
@@ -262,7 +289,7 @@ class PVForecastAkkudoktor(PVForecastProvider):
Raises:
ValueError: If the API response does not include expected `meta` data.
"""
response = requests.get(self._url())
response = requests.get(self._url(), timeout=10)
response.raise_for_status() # Raise an error for bad responses
logger.debug(f"Response from {self._url()}: {response}")
akkudoktor_data = self._validate_data(response.content)
@@ -303,7 +330,8 @@ class PVForecastAkkudoktor(PVForecastProvider):
logger.error(f"Akkudoktor schema change: {error_msg}")
raise ValueError(error_msg)
assert self.start_datetime # mypy fix
if not self.start_datetime:
raise ValueError(f"Start DateTime not set: {self.start_datetime}")
# Iterate over forecast data points
for forecast_values in zip(*akkudoktor_data.values):
@@ -390,28 +418,28 @@ if __name__ == "__main__":
"planes": [
{
"peakpower": 5.0,
"surface_azimuth": -10,
"surface_azimuth": 170,
"surface_tilt": 7,
"userhorizon": [20, 27, 22, 20],
"inverter_paco": 10000,
},
{
"peakpower": 4.8,
"surface_azimuth": -90,
"surface_azimuth": 90,
"surface_tilt": 7,
"userhorizon": [30, 30, 30, 50],
"inverter_paco": 10000,
},
{
"peakpower": 1.4,
"surface_azimuth": -40,
"surface_azimuth": 140,
"surface_tilt": 60,
"userhorizon": [60, 30, 0, 30],
"inverter_paco": 2000,
},
{
"peakpower": 1.6,
"surface_azimuth": 5,
"surface_azimuth": 185,
"surface_tilt": 45,
"userhorizon": [45, 25, 30, 60],
"inverter_paco": 1400,

View File

@@ -9,15 +9,13 @@ format, enabling consistent access to forecasted and historical pvforecast attri
from pathlib import Path
from typing import Optional, Union
from loguru import logger
from pydantic import Field, field_validator
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.prediction.predictionabc import PredictionImportProvider
from akkudoktoreos.prediction.pvforecastabc import PVForecastProvider
logger = get_logger(__name__)
class PVForecastImportCommonSettings(SettingsBaseModel):
"""Common settings for pvforecast data import from file or JSON string."""

View File

@@ -0,0 +1,110 @@
"""Retrieves pvforecast data from VRM API."""
from typing import Any, Optional, Union
import requests
from loguru import logger
from pendulum import DateTime
from pydantic import Field, ValidationError
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.pydantic import PydanticBaseModel
from akkudoktoreos.prediction.pvforecastabc import PVForecastProvider
from akkudoktoreos.utils.datetimeutil import to_datetime
class VrmForecastRecords(PydanticBaseModel):
vrm_consumption_fc: list[tuple[int, float]]
solar_yield_forecast: list[tuple[int, float]]
class VrmForecastResponse(PydanticBaseModel):
success: bool
records: VrmForecastRecords
totals: dict
class PVforecastVrmCommonSettings(SettingsBaseModel):
"""Common settings for VRM API."""
pvforecast_vrm_token: str = Field(
default="your-token", description="Token for Connecting VRM API", examples=["your-token"]
)
pvforecast_vrm_idsite: int = Field(
default=12345, description="VRM-Installation-ID", examples=[12345]
)
class PVForecastVrm(PVForecastProvider):
"""Fetch and process PV forecast data from VRM API."""
@classmethod
def provider_id(cls) -> str:
"""Return the unique identifier for the PV-Forecast-Provider."""
return "PVForecastVrm"
@classmethod
def _validate_data(cls, json_str: Union[bytes, Any]) -> VrmForecastResponse:
"""Validate the VRM forecast response data against the expected schema."""
try:
return VrmForecastResponse.model_validate_json(json_str)
except ValidationError as e:
error_msg = "\n".join(
f"Field: {' -> '.join(str(x) for x in err['loc'])}\n"
f"Error: {err['msg']}\nType: {err['type']}"
for err in e.errors()
)
logger.error(f"VRM-API schema change:\n{error_msg}")
raise ValueError(error_msg)
def _request_forecast(self, start_ts: int, end_ts: int) -> VrmForecastResponse:
"""Fetch forecast data from Victron VRM API."""
source = "https://vrmapi.victronenergy.com/v2/installations"
id_site = self.config.pvforecast.provider_settings.pvforecast_vrm_idsite
api_token = self.config.pvforecast.provider_settings.pvforecast_vrm_token
headers = {"X-Authorization": f"Token {api_token}", "Content-Type": "application/json"}
url = f"{source}/{id_site}/stats?type=forecast&start={start_ts}&end={end_ts}&interval=hours"
logger.debug(f"Requesting VRM forecast: {url}")
try:
response = requests.get(url, headers=headers, timeout=30)
response.raise_for_status()
except requests.RequestException as e:
logger.error(f"Failed to fetch pvforecast: {e}")
raise RuntimeError("Failed to fetch pvforecast from VRM API") from e
self.update_datetime = to_datetime(in_timezone=self.config.general.timezone)
return self._validate_data(response.content)
def _ts_to_datetime(self, timestamp: int) -> DateTime:
"""Convert UNIX ms timestamp to timezone-aware datetime."""
return to_datetime(timestamp / 1000, in_timezone=self.config.general.timezone)
def _update_data(self, force_update: Optional[bool] = False) -> None:
"""Update forecast data in the PVForecastDataRecord format."""
start_date = self.start_datetime.start_of("day")
end_date = self.start_datetime.add(hours=self.config.prediction.hours)
start_ts = int(start_date.timestamp())
end_ts = int(end_date.timestamp())
logger.info(f"Updating PV forecast from VRM: {start_date} to {end_date}")
vrm_forecast_data = self._request_forecast(start_ts, end_ts)
pv_forecast = []
for timestamp, value in vrm_forecast_data.records.solar_yield_forecast:
date = self._ts_to_datetime(timestamp)
dc_power = round(value, 2)
ac_power = round(dc_power * 0.96, 2)
self.update_value(
date, {"pvforecast_dc_power": dc_power, "pvforecast_ac_power": ac_power}
)
pv_forecast.append((date, dc_power))
logger.debug(f"Updated pvforecast_dc_power with {len(pv_forecast)} entries.")
self.update_datetime = to_datetime(in_timezone=self.config.general.timezone)
# Example usage
if __name__ == "__main__":
pv = PVForecastVrm()
pv._update_data()

View File

@@ -2,11 +2,22 @@
from typing import Optional
from pydantic import Field
from pydantic import Field, field_validator
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.prediction.prediction import get_prediction
from akkudoktoreos.prediction.weatherabc import WeatherProvider
from akkudoktoreos.prediction.weatherimport import WeatherImportCommonSettings
prediction_eos = get_prediction()
# Valid weather providers
weather_providers = [
provider.provider_id()
for provider in prediction_eos.providers
if isinstance(provider, WeatherProvider)
]
class WeatherCommonSettings(SettingsBaseModel):
"""Weather Forecast Configuration."""
@@ -20,3 +31,13 @@ class WeatherCommonSettings(SettingsBaseModel):
provider_settings: Optional[WeatherImportCommonSettings] = Field(
default=None, description="Provider settings", examples=[None]
)
# Validators
@field_validator("provider", mode="after")
@classmethod
def validate_provider(cls, value: Optional[str]) -> Optional[str]:
if value is None or value in weather_providers:
return value
raise ValueError(
f"Provider '{value}' is not a valid weather provider: {weather_providers}."
)

View File

@@ -14,11 +14,8 @@ import pandas as pd
import pvlib
from pydantic import Field
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.prediction.predictionabc import PredictionProvider, PredictionRecord
logger = get_logger(__name__)
class WeatherDataRecord(PredictionRecord):
"""Represents a weather data record containing various weather attributes at a specific datetime.

View File

@@ -9,18 +9,16 @@ format, enabling consistent access to forecasted and historical weather attribut
import json
from typing import Dict, List, Optional, Tuple, Union
import numpy as np
import pandas as pd
import pvlib
import requests
from loguru import logger
from akkudoktoreos.core.cache import cache_in_file
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.prediction.weatherabc import WeatherDataRecord, WeatherProvider
from akkudoktoreos.utils.datetimeutil import to_datetime, to_duration
logger = get_logger(__name__)
WheaterDataBrightSkyMapping: List[Tuple[str, Optional[str], Optional[Union[str, float]]]] = [
# brightsky_key, description, corr_factor
("timestamp", "DateTime", "to datetime in timezone"),
@@ -99,7 +97,8 @@ class WeatherBrightSky(WeatherProvider):
date = to_datetime(self.start_datetime, as_string=True)
last_date = to_datetime(self.end_datetime, as_string=True)
response = requests.get(
f"{source}/weather?lat={self.config.general.latitude}&lon={self.config.general.longitude}&date={date}&last_date={last_date}&tz={self.config.general.timezone}"
f"{source}/weather?lat={self.config.general.latitude}&lon={self.config.general.longitude}&date={date}&last_date={last_date}&tz={self.config.general.timezone}",
timeout=10,
)
response.raise_for_status() # Raise an error for bad responses
logger.debug(f"Response from {source}: {response}")
@@ -221,21 +220,31 @@ class WeatherBrightSky(WeatherProvider):
# Add Preciptable Water (PWAT) with a PVLib method.
key = WeatherDataRecord.key_from_description("Temperature (°C)")
assert key
assert key # noqa: S101
temperature = self.key_to_array(
key=key,
start_datetime=self.start_datetime,
end_datetime=self.end_datetime,
interval=to_duration("1 hour"),
)
if any(x is None or isinstance(x, float) and np.isnan(x) for x in temperature):
# We can not calculate PWAT
debug_msg = f"Innvalid temperature '{temperature}'"
logger.debug(debug_msg)
return
key = WeatherDataRecord.key_from_description("Relative Humidity (%)")
assert key
assert key # noqa: S101
humidity = self.key_to_array(
key=key,
start_datetime=self.start_datetime,
end_datetime=self.end_datetime,
interval=to_duration("1 hour"),
)
if any(x is None or isinstance(x, float) and np.isnan(x) for x in humidity):
# We can not calculate PWAT
debug_msg = f"Innvalid humidity '{humidity}'"
logger.debug(debug_msg)
return
data = pvlib.atmosphere.gueymard94_pw(temperature, humidity)
pwat = pd.Series(
data=data,

View File

@@ -18,15 +18,12 @@ from typing import Dict, List, Optional, Tuple
import pandas as pd
import requests
from bs4 import BeautifulSoup
from loguru import logger
from akkudoktoreos.core.cache import cache_in_file
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.prediction.weatherabc import WeatherDataRecord, WeatherProvider
from akkudoktoreos.utils.datetimeutil import to_datetime, to_duration, to_timezone
logger = get_logger(__name__)
WheaterDataClearOutsideMapping: List[Tuple[str, Optional[str], Optional[float]]] = [
# clearoutside_key, description, corr_factor
("DateTime", "DateTime", None),
@@ -88,12 +85,12 @@ class WeatherClearOutside(WeatherProvider):
"""Requests weather forecast from ClearOutside.
Returns:
response: Weather forecast request reponse from ClearOutside.
response: Weather forecast request response from ClearOutside.
"""
source = "https://clearoutside.com/forecast"
latitude = round(self.config.general.latitude, 2)
longitude = round(self.config.general.longitude, 2)
response = requests.get(f"{source}/{latitude}/{longitude}?desktop=true")
response = requests.get(f"{source}/{latitude}/{longitude}?desktop=true", timeout=10)
response.raise_for_status() # Raise an error for bad responses
logger.debug(f"Response from {source}: {response}")
# We are working on fresh data (no cache), report update time

View File

@@ -9,15 +9,13 @@ format, enabling consistent access to forecasted and historical weather attribut
from pathlib import Path
from typing import Optional, Union
from loguru import logger
from pydantic import Field, field_validator
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.prediction.predictionabc import PredictionImportProvider
from akkudoktoreos.prediction.weatherabc import WeatherProvider
logger = get_logger(__name__)
class WeatherImportCommonSettings(SettingsBaseModel):
"""Common settings for weather data import from file or JSON string."""

View File

@@ -0,0 +1,297 @@
"""Admin UI components for EOS Dashboard.
This module provides functions to generate administrative UI components
for the EOS dashboard.
"""
import json
from pathlib import Path
from typing import Any, Optional, Union
import requests
from fasthtml.common import Select
from loguru import logger
from monsterui.foundations import stringify
from monsterui.franken import ( # Select, TODO: Select from FrankenUI does not work - using Select from FastHTML instead
H3,
Button,
ButtonT,
Card,
Details,
Div,
DivHStacked,
DividerLine,
Grid,
Input,
Options,
P,
Summary,
UkIcon,
)
from platformdirs import user_config_dir
from akkudoktoreos.server.dash.components import Error, Success
from akkudoktoreos.server.dash.configuration import get_nested_value
from akkudoktoreos.utils.datetimeutil import to_datetime
# Directory to export files to, or to import files from
export_import_directory = Path(user_config_dir("net.akkudoktor.eosdash", "akkudoktor"))
def AdminButton(*c: Any, cls: Optional[Union[str, tuple]] = None, **kwargs: Any) -> Button:
"""Creates a styled button for administrative actions.
Args:
*c (Any): Positional arguments representing the button's content.
cls (Optional[Union[str, tuple]]): Additional CSS classes for styling. Defaults to None.
**kwargs (Any): Additional keyword arguments passed to the `Button`.
Returns:
Button: A styled `Button` component for admin actions.
"""
new_cls = f"{ButtonT.primary}"
if cls:
new_cls += f" {stringify(cls)}"
kwargs["cls"] = new_cls
return Button(*c, submit=False, **kwargs)
def AdminConfig(
eos_host: str, eos_port: Union[str, int], data: Optional[dict], config: Optional[dict[str, Any]]
) -> tuple[str, Union[Card, list[Card]]]:
"""Creates a configuration management card with save-to-file functionality.
Args:
eos_host (str): The hostname of the EOS server.
eos_port (Union[str, int]): The port of the EOS server.
data (Optional[dict]): Incoming data containing action and category for processing.
Returns:
tuple[str, Union[Card, list[Card]]]: A tuple containing the configuration category label and the `Card` UI component.
"""
server = f"http://{eos_host}:{eos_port}"
eos_hostname = "EOS server"
eosdash_hostname = "EOSdash server"
category = "configuration"
# save config file
status = (None,)
config_file_path = "<unknown>"
try:
if config:
config_file_path = get_nested_value(config, ["general", "config_file_path"])
except Exception as e:
logger.debug(f"general.config_file_path: {e}")
# export config file
export_to_file_next_tag = to_datetime(as_string="YYYYMMDDHHmmss")
export_to_file_status = (None,)
# import config file
import_from_file_status = (None,)
if data and data.get("category", None) == category:
# This data is for us
if data["action"] == "save_to_file":
# Safe current configuration to file
try:
result = requests.put(f"{server}/v1/config/file", timeout=10)
result.raise_for_status()
config_file_path = result.json()["general"]["config_file_path"]
status = Success(f"Saved to '{config_file_path}' on '{eos_hostname}'")
except requests.exceptions.HTTPError as e:
detail = result.json()["detail"]
status = Error(
f"Can not save actual config to file on '{eos_hostname}': {e}, {detail}"
)
except Exception as e:
status = Error(f"Can not save actual config to file on '{eos_hostname}': {e}")
elif data["action"] == "export_to_file":
# Export current configuration to file
export_to_file_tag = data.get("export_to_file_tag", export_to_file_next_tag)
export_to_file_path = export_import_directory.joinpath(
f"eos_config_{export_to_file_tag}.json"
)
try:
if not config:
raise ValueError(f"No config from '{eos_hostname}'")
export_to_file_path.parent.mkdir(parents=True, exist_ok=True)
with export_to_file_path.open("w", encoding="utf-8", newline="\n") as fd:
json.dump(config, fd, indent=4, sort_keys=True)
export_to_file_status = Success(
f"Exported to '{export_to_file_path}' on '{eosdash_hostname}'"
)
except requests.exceptions.HTTPError as e:
detail = result.json()["detail"]
export_to_file_status = Error(
f"Can not export actual config to '{export_to_file_path}' on '{eosdash_hostname}': {e}, {detail}"
)
except Exception as e:
export_to_file_status = Error(
f"Can not export actual config to '{export_to_file_path}' on '{eosdash_hostname}': {e}"
)
elif data["action"] == "import_from_file":
import_file_name = data.get("import_file_name", None)
import_from_file_pathes = list(
export_import_directory.glob("*.json")
) # expand generator object
import_file_path = None
for f in import_from_file_pathes:
if f.name == import_file_name:
import_file_path = f
if import_file_path:
try:
with import_file_path.open("r", encoding="utf-8", newline=None) as fd:
import_config = json.load(fd)
result = requests.put(f"{server}/v1/config", json=import_config, timeout=10)
result.raise_for_status()
import_from_file_status = Success(
f"Config imported from '{import_file_path}' on '{eosdash_hostname}'"
)
except requests.exceptions.HTTPError as e:
detail = result.json()["detail"]
import_from_file_status = Error(
f"Can not import config from '{import_file_name}' on '{eosdash_hostname}' {e}, {detail}"
)
except Exception as e:
import_from_file_status = Error(
f"Can not import config from '{import_file_name}' on '{eosdash_hostname}' {e}"
)
else:
import_from_file_status = Error(
f"Can not import config from '{import_file_name}', not found in '{export_import_directory}' on '{eosdash_hostname}'"
)
# Update for display, in case we added a new file before
import_from_file_names = [f.name for f in list(export_import_directory.glob("*.json"))]
return (
category,
[
Card(
Details(
Summary(
Grid(
DivHStacked(
UkIcon(icon="play"),
AdminButton(
"Save to file",
hx_post="/eosdash/admin",
hx_target="#page-content",
hx_swap="innerHTML",
hx_vals='{"category": "configuration", "action": "save_to_file"}',
),
P(f"'{config_file_path}' on '{eos_hostname}'"),
),
status,
),
cls="list-none",
),
P(f"Safe actual configuration to '{config_file_path}' on '{eos_hostname}'."),
),
),
Card(
Details(
Summary(
Grid(
DivHStacked(
UkIcon(icon="play"),
AdminButton(
"Export to file",
hx_post="/eosdash/admin",
hx_target="#page-content",
hx_swap="innerHTML",
hx_vals='js:{"category": "configuration", "action": "export_to_file", "export_to_file_tag": document.querySelector("[name=\'chosen_export_file_tag\']").value }',
),
P("'eos_config_"),
Input(
id="export_file_tag",
name="chosen_export_file_tag",
value=export_to_file_next_tag,
),
P(".json'"),
),
export_to_file_status,
),
cls="list-none",
),
P(
f"Export actual configuration to 'eos_config_{export_to_file_next_tag}.json' on '{eosdash_hostname}'."
),
),
),
Card(
Details(
Summary(
Grid(
DivHStacked(
UkIcon(icon="play"),
AdminButton(
"Import from file",
hx_post="/eosdash/admin",
hx_target="#page-content",
hx_swap="innerHTML",
hx_vals='js:{ "category": "configuration", "action": "import_from_file", "import_file_name": document.querySelector("[name=\'selected_import_file_name\']").value }',
),
Select(
*Options(*import_from_file_names),
id="import_file_name",
name="selected_import_file_name", # Name of hidden input field with selected value
placeholder="Select file",
),
),
import_from_file_status,
),
cls="list-none",
),
P(f"Import configuration from config file on '{eosdash_hostname}'."),
),
),
],
)
def Admin(eos_host: str, eos_port: Union[str, int], data: Optional[dict] = None) -> Div:
"""Generates the administrative dashboard layout.
This includes configuration management and other administrative tools.
Args:
eos_host (str): The hostname of the EOS server.
eos_port (Union[str, int]): The port of the EOS server.
data (Optional[dict], optional): Incoming data to trigger admin actions. Defaults to None.
Returns:
Div: A `Div` component containing the assembled admin interface.
"""
# Get current configuration from server
server = f"http://{eos_host}:{eos_port}"
try:
result = requests.get(f"{server}/v1/config", timeout=10)
result.raise_for_status()
config = result.json()
except requests.exceptions.HTTPError as e:
config = {}
detail = result.json()["detail"]
warning_msg = f"Can not retrieve configuration from {server}: {e}, {detail}"
logger.warning(warning_msg)
return Error(warning_msg)
except Exception as e:
warning_msg = f"Can not retrieve configuration from {server}: {e}"
logger.warning(warning_msg)
return Error(warning_msg)
rows = []
last_category = ""
for category, admin in [
AdminConfig(eos_host, eos_port, data, config),
]:
if category != last_category:
rows.append(H3(category))
rows.append(DividerLine())
last_category = category
if isinstance(admin, list):
for card in admin:
rows.append(card)
else:
rows.append(admin)
return Div(*rows, cls="space-y-4")

View File

@@ -8,19 +8,19 @@ from bokeh.models import Plot
from monsterui.franken import H4, Card, NotStr, Script
BokehJS = [
Script(src="https://cdn.bokeh.org/bokeh/release/bokeh-3.6.3.min.js", crossorigin="anonymous"),
Script(src="https://cdn.bokeh.org/bokeh/release/bokeh-3.7.0.min.js", crossorigin="anonymous"),
Script(
src="https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.6.3.min.js",
src="https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.7.0.min.js",
crossorigin="anonymous",
),
Script(
src="https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.6.3.min.js", crossorigin="anonymous"
src="https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.7.0.min.js", crossorigin="anonymous"
),
Script(
src="https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.6.3.min.js", crossorigin="anonymous"
src="https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.7.0.min.js", crossorigin="anonymous"
),
Script(
src="https://cdn.bokeh.org/bokeh/release/bokeh-mathjax-3.6.3.min.js",
src="https://cdn.bokeh.org/bokeh/release/bokeh-mathjax-3.7.0.min.js",
crossorigin="anonymous",
),
]

View File

@@ -1,10 +1,13 @@
from typing import Any, Optional, Union
from fasthtml.common import H1, Div, Li
# from mdit_py_plugins import plugin1, plugin2
from monsterui.daisy import (
Alert,
AlertT,
)
from monsterui.foundations import stringify
from monsterui.franken import (
H3,
Button,
ButtonT,
Card,
@@ -13,6 +16,7 @@ from monsterui.franken import (
Details,
DivLAligned,
DivRAligned,
Form,
Grid,
Input,
P,
@@ -69,9 +73,69 @@ def ScrollArea(
)
def Success(*c: Any) -> Alert:
return Alert(
DivLAligned(
UkIcon("check"),
P(*c),
),
cls=AlertT.success,
)
def Error(*c: Any) -> Alert:
return Alert(
DivLAligned(
UkIcon("triangle-alert"),
P(*c),
),
cls=AlertT.error,
)
def ConfigCard(
config_name: str, config_type: str, read_only: str, value: str, default: str, description: str
config_name: str,
config_type: str,
read_only: str,
value: str,
default: str,
description: str,
deprecated: Optional[Union[str, bool]],
update_error: Optional[str],
update_value: Optional[str],
update_open: Optional[bool],
) -> Card:
"""Creates a styled configuration card for displaying configuration details.
This function generates a configuration card that is displayed in the UI with
various sections such as configuration name, type, description, default value,
current value, and error details. It supports both read-only and editable modes.
Args:
config_name (str): The name of the configuration.
config_type (str): The type of the configuration.
read_only (str): Indicates if the configuration is read-only ("rw" for read-write,
any other value indicates read-only).
value (str): The current value of the configuration.
default (str): The default value of the configuration.
description (str): A description of the configuration.
deprecated (Optional[Union[str, bool]]): The deprecated marker of the configuration.
update_error (Optional[str]): The error message, if any, during the update process.
update_value (Optional[str]): The value to be updated, if different from the current value.
update_open (Optional[bool]): A flag indicating whether the update section of the card
should be initially expanded.
Returns:
Card: A styled Card component containing the configuration details.
"""
config_id = config_name.replace(".", "-")
if not update_value:
update_value = value
if not update_open:
update_open = False
if deprecated:
if isinstance(deprecated, bool):
deprecated = "Deprecated"
return Card(
Details(
Summary(
@@ -85,24 +149,53 @@ def ConfigCard(
P(read_only),
),
),
Input(value=value) if read_only == "rw" else P(value),
P(value),
),
# cls="flex cursor-pointer list-none items-center gap-4",
cls="list-none",
),
Grid(
P(description),
P(config_type),
),
Grid(
DivRAligned(
P("default") if read_only == "rw" else P(""),
),
P(default) if read_only == "rw" else P(""),
)
if read_only == "rw"
if not deprecated
else None,
Grid(
P(deprecated),
P("DEPRECATED!"),
)
if deprecated
else None,
# Default
Grid(
DivRAligned(P("default")),
P(default),
)
if read_only == "rw" and not deprecated
else None,
# Set value
Grid(
DivRAligned(P("update")),
Grid(
Form(
Input(value=config_name, type="hidden", id="key"),
Input(value=update_value, type="text", id="value"),
hx_put="/eosdash/configuration",
hx_target="#page-content",
hx_swap="innerHTML",
),
),
)
if read_only == "rw" and not deprecated
else None,
# Last error
Grid(
DivRAligned(P("update error")),
P(update_error),
)
if update_error
else None,
cls="space-y-4 gap-4",
open=update_open,
),
cls="w-full",
)
@@ -173,7 +266,7 @@ def DashboardTabs(dashboard_items: dict[str, str]) -> Card:
dash_items = [
Li(
DashboardTrigger(
menu,
H3(menu),
hx_get=f"{path}",
hx_target="#page-content",
hx_swap="innerHTML",

View File

@@ -1,20 +1,38 @@
import json
from typing import Any, Dict, List, Optional, Sequence, TypeVar, Union
import requests
from monsterui.franken import Div, DividerLine, P, Table, Tbody, Td, Th, Thead, Tr
from loguru import logger
from monsterui.franken import (
H3,
H4,
Card,
Details,
Div,
DividerLine,
DivLAligned,
DivRAligned,
Form,
Grid,
Input,
P,
Summary,
UkIcon,
)
from pydantic.fields import ComputedFieldInfo, FieldInfo
from pydantic_core import PydanticUndefined
from akkudoktoreos.config.config import get_config
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.config.config import ConfigEOS
from akkudoktoreos.core.pydantic import PydanticBaseModel
from akkudoktoreos.prediction.pvforecast import PVForecastPlaneSetting
from akkudoktoreos.server.dash.components import ConfigCard
logger = get_logger(__name__)
config_eos = get_config()
T = TypeVar("T")
# Latest configuration update results
# Dictionary of config names and associated dictionary with keys "value", "result", "error", "open".
config_update_latest: dict[str, dict[str, Optional[Union[str, bool]]]] = {}
def get_nested_value(
dictionary: Union[Dict[str, Any], List[Any]],
@@ -48,10 +66,10 @@ def get_nested_value(
# Traverse the structure
current = dictionary
for key in keys:
if isinstance(current, dict) and isinstance(key, str):
current = current[key]
elif isinstance(current, list) and isinstance(key, int):
current = current[key]
if isinstance(current, dict):
current = current[str(key)]
elif isinstance(current, list):
current = current[int(key)]
else:
raise KeyError(f"Invalid key or index: {key}")
return current
@@ -101,25 +119,36 @@ def resolve_nested_types(field_type: Any, parent_types: list[str]) -> list[tuple
return resolved_types
def configuration(values: dict) -> list[dict]:
def configuration(
model: type[PydanticBaseModel], values: dict, values_prefix: list[str] = []
) -> list[dict]:
"""Generate configuration details based on provided values and model metadata.
Args:
model (type[PydanticBaseModel]): The Pydantic model to extract configuration from.
values (dict): A dictionary containing the current configuration values.
values_prefix (list[str]): A list of parent type names that prefixes the model values in the values.
Returns:
List[dict]: A sorted list of configuration details, each represented as a dictionary.
list[dict]: A sorted list of configuration details, each represented as a dictionary.
"""
configs = []
inner_types: set[type[PydanticBaseModel]] = set()
for field_name, field_info in list(config_eos.model_fields.items()) + list(
config_eos.model_computed_fields.items()
for field_name, field_info in list(model.model_fields.items()) + list(
model.model_computed_fields.items()
):
def extract_nested_models(
subfield_info: Union[ComputedFieldInfo, FieldInfo], parent_types: list[str]
) -> None:
"""Extract nested models from the given subfield information.
Args:
subfield_info (Union[ComputedFieldInfo, FieldInfo]): Field metadata from Pydantic.
parent_types (list[str]): A list of parent type names for hierarchical representation.
"""
nonlocal values, values_prefix
regular_field = isinstance(subfield_info, FieldInfo)
subtype = subfield_info.annotation if regular_field else subfield_info.return_type
@@ -135,13 +164,18 @@ def configuration(values: dict) -> list[dict]:
if found_basic:
continue
config = {}
config["name"] = ".".join(parent_types)
config["value"] = str(get_nested_value(values, parent_types, "<unknown>"))
config["default"] = str(get_default_value(subfield_info, regular_field))
config: dict[str, Optional[Any]] = {}
config["name"] = ".".join(values_prefix + parent_types)
config["value"] = json.dumps(
get_nested_value(values, values_prefix + parent_types, "<unknown>")
)
config["default"] = json.dumps(get_default_value(subfield_info, regular_field))
config["description"] = (
subfield_info.description if subfield_info.description else ""
)
config["deprecated"] = (
subfield_info.deprecated if subfield_info.deprecated else None
)
if isinstance(subfield_info, ComputedFieldInfo):
config["read-only"] = "ro"
type_description = str(subfield_info.return_type)
@@ -151,8 +185,8 @@ def configuration(values: dict) -> list[dict]:
config["type"] = (
type_description.replace("typing.", "")
.replace("pathlib.", "")
.replace("[", "[ ")
.replace("NoneType", "None")
.replace("<class 'float'>", "float")
)
configs.append(config)
found_basic = True
@@ -171,105 +205,345 @@ def configuration(values: dict) -> list[dict]:
return sorted(configs, key=lambda x: x["name"])
def get_configuration(eos_host: Optional[str], eos_port: Optional[Union[str, int]]) -> list[dict]:
def get_configuration(eos_host: str, eos_port: Union[str, int]) -> list[dict]:
"""Fetch and process configuration data from the specified EOS server.
Args:
eos_host (Optional[str]): The hostname of the server.
eos_port (Optional[Union[str, int]]): The port of the server.
eos_host (str): The hostname of the EOS server.
eos_port (Union[str, int]): The port of the EOS server.
Returns:
List[dict]: A list of processed configuration entries.
"""
if eos_host is None:
eos_host = config_eos.server.host
if eos_port is None:
eos_port = config_eos.server.port
server = f"http://{eos_host}:{eos_port}"
# Get current configuration from server
try:
result = requests.get(f"{server}/v1/config")
result = requests.get(f"{server}/v1/config", timeout=10)
result.raise_for_status()
config = result.json()
except requests.exceptions.HTTPError as e:
config = {}
detail = result.json()["detail"]
warning_msg = f"Can not retrieve configuration from {server}: {e}, {detail}"
logger.warning(warning_msg)
return configuration({})
config = result.json()
return configuration(config)
return configuration(ConfigEOS, config)
def Configuration(eos_host: Optional[str], eos_port: Optional[Union[str, int]]) -> Div:
def ConfigPlanesCard(
config_name: str,
config_type: str,
read_only: str,
value: str,
default: str,
description: str,
max_planes: int,
update_error: Optional[str],
update_value: Optional[str],
update_open: Optional[bool],
) -> Card:
"""Creates a styled configuration card for PV planes.
This function generates a configuration card that is displayed in the UI with
various sections such as configuration name, type, description, default value,
current value, and error details. It supports both read-only and editable modes.
Args:
config_name (str): The name of the PV planes configuration.
config_type (str): The type of the PV planes configuration.
read_only (str): Indicates if the PV planes configuration is read-only ("rw" for read-write,
any other value indicates read-only).
value (str): The current value of the PV planes configuration.
default (str): The default value of the PV planes configuration.
description (str): A description of the PV planes configuration.
max_planes (int): Maximum number of planes that can be set
update_error (Optional[str]): The error message, if any, during the update process.
update_value (Optional[str]): The value to be updated, if different from the current value.
update_open (Optional[bool]): A flag indicating whether the update section of the card
should be initially expanded.
Returns:
Card: A styled Card component containing the PV planes configuration details.
"""
config_id = config_name.replace(".", "-")
# Remember overall planes update status
planes_update_error = update_error
planes_update_value = update_value
if not planes_update_value:
planes_update_value = value
planes_update_open = update_open
if not planes_update_open:
planes_update_open = False
# Create EOS planes configuration
eos_planes = json.loads(value)
eos_planes_config = {
"pvforecast": {
"planes": eos_planes,
},
}
# Create cards for all planes
rows = []
for i in range(0, max_planes):
plane_config = configuration(
PVForecastPlaneSetting(),
eos_planes_config,
values_prefix=["pvforecast", "planes", str(i)],
)
plane_rows = []
plane_update_open = False
if eos_planes and len(eos_planes) > i:
plane_value = json.dumps(eos_planes[i])
else:
plane_value = json.dumps(None)
for config in plane_config:
update_error = config_update_latest.get(config["name"], {}).get("error") # type: ignore
update_value = config_update_latest.get(config["name"], {}).get("value") # type: ignore
update_open = config_update_latest.get(config["name"], {}).get("open") # type: ignore
if update_open:
planes_update_open = True
plane_update_open = True
# Make mypy happy - should never trigger
if (
not isinstance(update_error, (str, type(None)))
or not isinstance(update_value, (str, type(None)))
or not isinstance(update_open, (bool, type(None)))
):
error_msg = "update_error or update_value or update_open of wrong type."
logger.error(error_msg)
raise TypeError(error_msg)
plane_rows.append(
ConfigCard(
config["name"],
config["type"],
config["read-only"],
config["value"],
config["default"],
config["description"],
config["deprecated"],
update_error,
update_value,
update_open,
)
)
rows.append(
Card(
Details(
Summary(
Grid(
Grid(
DivLAligned(
UkIcon(icon="play"),
H4(f"pvforecast.planes.{i}"),
),
DivRAligned(
P(read_only),
),
),
P(plane_value),
),
cls="list-none",
),
*plane_rows,
cls="space-y-4 gap-4",
open=plane_update_open,
),
cls="w-full",
)
)
return Card(
Details(
Summary(
Grid(
Grid(
DivLAligned(
UkIcon(icon="play"),
P(config_name),
),
DivRAligned(
P(read_only),
),
),
P(value),
),
cls="list-none",
),
Grid(
P(description),
P(config_type),
),
# Default
Grid(
DivRAligned(P("default")),
P(default),
)
if read_only == "rw"
else None,
# Set value
Grid(
DivRAligned(P("update")),
Grid(
Form(
Input(value=config_name, type="hidden", id="key"),
Input(value=planes_update_value, type="text", id="value"),
hx_put="/eosdash/configuration",
hx_target="#page-content",
hx_swap="innerHTML",
),
),
)
if read_only == "rw"
else None,
# Last error
Grid(
DivRAligned(P("update error")),
P(planes_update_error),
)
if planes_update_error
else None,
# Now come the single element configs
*rows,
cls="space-y-4 gap-4",
open=planes_update_open,
),
cls="w-full",
)
def Configuration(
eos_host: str, eos_port: Union[str, int], configuration: Optional[list[dict]] = None
) -> Div:
"""Create a visual representation of the configuration.
Args:
eos_host (Optional[str]): The hostname of the EOS server.
eos_port (Optional[Union[str, int]]): The port of the EOS server.
eos_host (str): The hostname of the EOS server.
eos_port (Union[str, int]): The port of the EOS server.
configuration (Optional[list[dict]]): Optional configuration. If not provided it will be
retrievd from EOS.
Returns:
Table: A `monsterui.franken.Table` component displaying configuration details.
rows: Rows of configuration details.
"""
flds = "Name", "Type", "RO/RW", "Value", "Default", "Description"
if not configuration:
configuration = get_configuration(eos_host, eos_port)
rows = []
last_category = ""
for config in get_configuration(eos_host, eos_port):
# find some special configuration values
max_planes = 0
for config in configuration:
if config["name"] == "pvforecast.max_planes":
try:
max_planes = int(config["value"])
except:
max_planes = 0
# build visual representation
for config in configuration:
category = config["name"].split(".")[0]
if category != last_category:
rows.append(P(category))
rows.append(H3(category))
rows.append(DividerLine())
last_category = category
rows.append(
ConfigCard(
config["name"],
config["type"],
config["read-only"],
config["value"],
config["default"],
config["description"],
update_error = config_update_latest.get(config["name"], {}).get("error")
update_value = config_update_latest.get(config["name"], {}).get("value")
update_open = config_update_latest.get(config["name"], {}).get("open")
# Make mypy happy - should never trigger
if (
not isinstance(update_error, (str, type(None)))
or not isinstance(update_value, (str, type(None)))
or not isinstance(update_open, (bool, type(None)))
):
error_msg = "update_error or update_value or update_open of wrong type."
logger.error(error_msg)
raise TypeError(error_msg)
if (
config["type"]
== "Optional[list[akkudoktoreos.prediction.pvforecast.PVForecastPlaneSetting]]"
and not config["deprecated"]
):
# Special configuration for PV planes
rows.append(
ConfigPlanesCard(
config["name"],
config["type"],
config["read-only"],
config["value"],
config["default"],
config["description"],
max_planes,
update_error,
update_value,
update_open,
)
)
else:
rows.append(
ConfigCard(
config["name"],
config["type"],
config["read-only"],
config["value"],
config["default"],
config["description"],
config["deprecated"],
update_error,
update_value,
update_open,
)
)
)
return Div(*rows, cls="space-y-4")
def ConfigurationOrg(eos_host: Optional[str], eos_port: Optional[Union[str, int]]) -> Table:
"""Create a visual representation of the configuration.
def ConfigKeyUpdate(eos_host: str, eos_port: Union[str, int], key: str, value: str) -> P:
"""Update configuration key and create a visual representation of the configuration.
Args:
eos_host (Optional[str]): The hostname of the EOS server.
eos_port (Optional[Union[str, int]]): The port of the EOS server.
eos_host (str): The hostname of the EOS server.
eos_port (Union[str, int]): The port of the EOS server.
key (str): configuration key in dot notation
value (str): configuration value as json string
Returns:
Table: A `monsterui.franken.Table` component displaying configuration details.
rows: Rows of configuration details.
"""
flds = "Name", "Type", "RO/RW", "Value", "Default", "Description"
rows = [
Tr(
Td(
config["name"],
cls="max-w-64 text-wrap break-all",
),
Td(
config["type"],
cls="max-w-48 text-wrap break-all",
),
Td(
config["read-only"],
cls="max-w-24 text-wrap break-all",
),
Td(
config["value"],
cls="max-w-md text-wrap break-all",
),
Td(config["default"], cls="max-w-48 text-wrap break-all"),
Td(
config["description"],
cls="max-w-prose text-wrap",
),
cls="",
)
for config in get_configuration(eos_host, eos_port)
]
head = Thead(*map(Th, flds), cls="text-left")
return Table(head, Tbody(*rows), cls="w-full uk-table uk-table-divider uk-table-striped")
server = f"http://{eos_host}:{eos_port}"
path = key.replace(".", "/")
try:
data = json.loads(value)
except:
if value in ("None", "none", "Null", "null"):
data = None
else:
data = value
error = None
config = None
try:
response = requests.put(f"{server}/v1/config/{path}", json=data, timeout=10)
response.raise_for_status()
config = response.json()
except requests.exceptions.HTTPError as err:
try:
# Try to get 'detail' from the JSON response
detail = response.json().get(
"detail", f"No error details for data '{data}' '{response.text}'"
)
except ValueError:
# Response is not JSON
detail = f"No error details for data '{data}' '{response.text}'"
error = f"Can not set {key} on {server}: {err}, {detail}"
# Mark all updates as closed
for k in config_update_latest:
config_update_latest[k]["open"] = False
# Remember this update as latest one
config_update_latest[key] = {
"error": error,
"result": config,
"value": value,
"open": True,
}
if error or config is None:
# Reread configuration to be shure we display actual data
return Configuration(eos_host, eos_port)
# Use configuration already provided
return Configuration(eos_host, eos_port, configuration(ConfigEOS, config))

View File

@@ -24,7 +24,7 @@
"planes": [
{
"peakpower": 5.0,
"surface_azimuth": -10,
"surface_azimuth": 170,
"surface_tilt": 7,
"userhorizon": [
20,
@@ -36,7 +36,7 @@
},
{
"peakpower": 4.8,
"surface_azimuth": -90,
"surface_azimuth": 90,
"surface_tilt": 7,
"userhorizon": [
30,
@@ -48,7 +48,7 @@
},
{
"peakpower": 1.4,
"surface_azimuth": -40,
"surface_azimuth": 140,
"surface_tilt": 60,
"userhorizon": [
60,
@@ -60,7 +60,7 @@
},
{
"peakpower": 1.6,
"surface_azimuth": 5,
"surface_azimuth": 185,
"surface_tilt": 45,
"userhorizon": [
45,
@@ -75,9 +75,9 @@
},
"server": {
"startup_eosdash": true,
"host": "0.0.0.0",
"host": "127.0.0.1",
"port": 8503,
"eosdash_host": "0.0.0.0",
"eosdash_host": "127.0.0.1",
"eosdash_port": 8504
},
"weather": {

View File

@@ -8,7 +8,6 @@ from bokeh.models import ColumnDataSource, LinearAxis, Range1d
from bokeh.plotting import figure
from monsterui.franken import FT, Grid, P
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.core.pydantic import PydanticDateTimeDataFrame
from akkudoktoreos.server.dash.bokeh import Bokeh
@@ -17,8 +16,6 @@ FILE_DEMOCONFIG = DIR_DEMODATA.joinpath("democonfig.json")
if not FILE_DEMOCONFIG.exists():
raise ValueError(f"File does not exist: {FILE_DEMOCONFIG}")
logger = get_logger(__name__)
# bar width for 1 hour bars (time given in millseconds)
BAR_WIDTH_1HOUR = 1000 * 60 * 60
@@ -76,25 +73,37 @@ def DemoElectricityPriceForecast(predictions: pd.DataFrame, config: dict) -> FT:
return Bokeh(plot)
def DemoWeatherTempAir(predictions: pd.DataFrame, config: dict) -> FT:
def DemoWeatherTempAirHumidity(predictions: pd.DataFrame, config: dict) -> FT:
source = ColumnDataSource(predictions)
provider = config["weather"]["provider"]
plot = figure(
x_axis_type="datetime",
y_range=Range1d(
predictions["weather_temp_air"].min() - 1.0, predictions["weather_temp_air"].max() + 1.0
),
title=f"Air Temperature Prediction ({provider})",
title=f"Air Temperature and Humidity Prediction ({provider})",
x_axis_label="Datetime",
y_axis_label="Temperature [°C]",
sizing_mode="stretch_width",
height=400,
)
# Add secondary y-axis for humidity
plot.extra_y_ranges["humidity"] = Range1d(start=-5, end=105)
y2_axis = LinearAxis(y_range_name="humidity", axis_label="Relative Humidity [%]")
y2_axis.axis_label_text_color = "green"
plot.add_layout(y2_axis, "left")
plot.line(
"date_time", "weather_temp_air", source=source, legend_label="Air Temperature", color="blue"
)
plot.line(
"date_time",
"weather_relative_humidity",
source=source,
legend_label="Relative Humidity [%]",
color="green",
y_range_name="humidity",
)
return Bokeh(plot)
@@ -150,7 +159,10 @@ def DemoLoad(predictions: pd.DataFrame, config: dict) -> FT:
sizing_mode="stretch_width",
height=400,
)
plot.extra_y_ranges["stddev"] = Range1d(0, 1000)
# Add secondary y-axis for stddev
stddev_min = predictions["load_std"].min()
stddev_max = predictions["load_std"].max()
plot.extra_y_ranges["stddev"] = Range1d(start=stddev_min - 5, end=stddev_max + 5)
y2_axis = LinearAxis(y_range_name="stddev", axis_label="Load Standard Deviation [W]")
y2_axis.axis_label_text_color = "green"
plot.add_layout(y2_axis, "left")
@@ -186,7 +198,7 @@ def Demo(eos_host: str, eos_port: Union[str, int]) -> str:
# Get current configuration from server
try:
result = requests.get(f"{server}/v1/config")
result = requests.get(f"{server}/v1/config", timeout=10)
result.raise_for_status()
except requests.exceptions.HTTPError as err:
detail = result.json()["detail"]
@@ -200,12 +212,12 @@ def Demo(eos_host: str, eos_port: Union[str, int]) -> str:
with FILE_DEMOCONFIG.open("r", encoding="utf-8") as fd:
democonfig = json.load(fd)
try:
result = requests.put(f"{server}/v1/config", json=democonfig)
result = requests.put(f"{server}/v1/config", json=democonfig, timeout=10)
result.raise_for_status()
except requests.exceptions.HTTPError as err:
detail = result.json()["detail"]
# Try to reset to original config
requests.put(f"{server}/v1/config", json=config)
requests.put(f"{server}/v1/config", json=config, timeout=10)
return P(
f"Can not set demo configuration on {server}: {err}, {detail}",
cls="text-center",
@@ -213,12 +225,12 @@ def Demo(eos_host: str, eos_port: Union[str, int]) -> str:
# Update all predictions
try:
result = requests.post(f"{server}/v1/prediction/update")
result = requests.post(f"{server}/v1/prediction/update", timeout=10)
result.raise_for_status()
except requests.exceptions.HTTPError as err:
detail = result.json()["detail"]
# Try to reset to original config
requests.put(f"{server}/v1/config", json=config)
requests.put(f"{server}/v1/config", json=config, timeout=10)
return P(
f"Can not update predictions on {server}: {err}, {detail}",
cls="text-center",
@@ -230,6 +242,7 @@ def Demo(eos_host: str, eos_port: Union[str, int]) -> str:
"keys": [
"pvforecast_ac_power",
"elecprice_marketprice_kwh",
"weather_relative_humidity",
"weather_temp_air",
"weather_ghi",
"weather_dni",
@@ -239,7 +252,7 @@ def Demo(eos_host: str, eos_port: Union[str, int]) -> str:
"load_mean_adjusted",
],
}
result = requests.get(f"{server}/v1/prediction/dataframe", params=params)
result = requests.get(f"{server}/v1/prediction/dataframe", params=params, timeout=10)
result.raise_for_status()
predictions = PydanticDateTimeDataFrame(**result.json()).to_dataframe()
except requests.exceptions.HTTPError as err:
@@ -255,12 +268,12 @@ def Demo(eos_host: str, eos_port: Union[str, int]) -> str:
)
# Reset to original config
requests.put(f"{server}/v1/config", json=config)
requests.put(f"{server}/v1/config", json=config, timeout=10)
return Grid(
DemoPVForecast(predictions, democonfig),
DemoElectricityPriceForecast(predictions, democonfig),
DemoWeatherTempAir(predictions, democonfig),
DemoWeatherTempAirHumidity(predictions, democonfig),
DemoWeatherIrradiance(predictions, democonfig),
DemoLoad(predictions, democonfig),
cols_max=2,

View File

@@ -1,14 +1,13 @@
from typing import Optional, Union
import requests
from loguru import logger
from monsterui.daisy import Loading, LoadingT
from monsterui.franken import A, ButtonT, DivFullySpaced, P
from requests.exceptions import RequestException
from akkudoktoreos.config.config import get_config
from akkudoktoreos.core.logging import get_logger
logger = get_logger(__name__)
config_eos = get_config()
@@ -24,7 +23,7 @@ def get_alive(eos_host: str, eos_port: Union[str, int]) -> str:
"""
result = requests.Response()
try:
result = requests.get(f"http://{eos_host}:{eos_port}/v1/health")
result = requests.get(f"http://{eos_host}:{eos_port}/v1/health", timeout=10)
if result.status_code == 200:
alive = result.json()["status"]
else:

View File

@@ -25,11 +25,13 @@ from fastapi.responses import (
RedirectResponse,
Response,
)
from loguru import logger
from akkudoktoreos.config.config import ConfigEOS, SettingsEOS, get_config
from akkudoktoreos.core.cache import CacheFileStore
from akkudoktoreos.core.ems import get_ems
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.core.logabc import LOGGING_LEVELS
from akkudoktoreos.core.logging import read_file_log, track_logging_config
from akkudoktoreos.core.pydantic import (
PydanticBaseModel,
PydanticDateTimeData,
@@ -49,10 +51,13 @@ from akkudoktoreos.prediction.prediction import PredictionCommonSettings, get_pr
from akkudoktoreos.prediction.pvforecast import PVForecastCommonSettings
from akkudoktoreos.server.rest.error import create_error_page
from akkudoktoreos.server.rest.tasks import repeat_every
from akkudoktoreos.server.server import get_default_host, wait_for_port_free
from akkudoktoreos.server.server import (
get_default_host,
is_valid_ip_or_hostname,
wait_for_port_free,
)
from akkudoktoreos.utils.datetimeutil import to_datetime, to_duration
logger = get_logger(__name__)
config_eos = get_config()
measurement_eos = get_measurement()
prediction_eos = get_prediction()
@@ -62,6 +67,14 @@ ems_eos = get_ems()
args = None
# ----------------------
# Logging configuration
# ----------------------
logger.remove()
track_logging_config(config_eos, "logging", None, None)
config_eos.track_nested_value("/logging", track_logging_config)
# ----------------------
# EOSdash server startup
# ----------------------
@@ -100,6 +113,11 @@ def start_eosdash(
Raises:
RuntimeError: If the EOSdash server fails to start.
"""
if not is_valid_ip_or_hostname(host):
raise ValueError(f"Invalid EOSdash host: {host}")
if not is_valid_ip_or_hostname(eos_host):
raise ValueError(f"Invalid EOS host: {eos_host}")
eosdash_path = Path(__file__).parent.resolve().joinpath("eosdash.py")
# Do a one time check for port free to generate warnings if not so
@@ -130,7 +148,7 @@ def start_eosdash(
env["EOS_CONFIG_DIR"] = eos_config_dir
try:
server_process = subprocess.Popen(
server_process = subprocess.Popen( # noqa: S603
cmd,
env=env,
stdout=subprocess.PIPE,
@@ -168,18 +186,32 @@ def cache_save() -> dict:
return CacheFileStore().save_store()
@repeat_every(seconds=float(config_eos.cache.cleanup_interval))
def cache_cleanup_on_exception(e: Exception) -> None:
logger.error("Cache cleanup task caught an exception: {}", e, exc_info=True)
@repeat_every(
seconds=float(config_eos.cache.cleanup_interval),
on_exception=cache_cleanup_on_exception,
)
def cache_cleanup_task() -> None:
"""Repeating task to clear cache from expired cache files."""
logger.debug("Clear cache")
cache_clear()
def energy_management_on_exception(e: Exception) -> None:
logger.error("Energy management task caught an exception: {}", e, exc_info=True)
@repeat_every(
seconds=10,
wait_first=config_eos.ems.startup_delay,
on_exception=energy_management_on_exception,
)
def energy_management_task() -> None:
"""Repeating task for energy management."""
logger.debug("Check EMS run")
ems_eos.manage_energy()
@@ -210,6 +242,7 @@ async def server_shutdown_task() -> None:
os.kill(pid, signal.SIGTERM) # type: ignore[attr-defined,unused-ignore]
logger.info(f"🚀 EOS terminated, PID {pid}")
sys.exit(0)
@asynccontextmanager
@@ -240,10 +273,10 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
access_log = args.access_log
reload = args.reload
host = host if host else get_default_host()
port = port if port else 8504
eos_host = eos_host if eos_host else get_default_host()
eos_port = eos_port if eos_port else 8503
host = host if host else eos_host
port = port if port else 8504
eos_dir = str(config_eos.general.data_folder_path)
eos_config_dir = str(config_eos.general.config_folder_path)
@@ -370,7 +403,7 @@ async def fastapi_admin_server_restart_post() -> dict:
env["EOS_DIR"] = str(config_eos.general.data_folder_path)
env["EOS_CONFIG_DIR"] = str(config_eos.general.config_folder_path)
new_process = subprocess.Popen(
new_process = subprocess.Popen( # noqa: S603
[
sys.executable,
]
@@ -486,7 +519,9 @@ def fastapi_config_put_key(
path: str = FastapiPath(
..., description="The nested path to the configuration key (e.g., general/latitude)."
),
value: Any = Body(..., description="The value to assign to the specified configuration path."),
value: Optional[Any] = Body(
None, description="The value to assign to the specified configuration path (can be None)."
),
) -> ConfigEOS:
"""Update a nested key or index in the config model.
@@ -498,13 +533,13 @@ def fastapi_config_put_key(
configuration (ConfigEOS): The current configuration after the update.
"""
try:
config_eos.set_config_value(path, value)
except IndexError as e:
raise HTTPException(status_code=400, detail=str(e))
except KeyError as e:
raise HTTPException(status_code=404, detail=str(e))
config_eos.set_nested_value(path, value)
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
trace = "".join(traceback.TracebackException.from_exception(e).format())
raise HTTPException(
status_code=400,
detail=f"Error on update of configuration '{path}','{value}': {e}\n{trace}",
)
return config_eos
@@ -524,7 +559,7 @@ def fastapi_config_get_key(
value (Any): The value of the selected nested key.
"""
try:
return config_eos.get_config_value(path)
return config_eos.get_nested_value(path)
except IndexError as e:
raise HTTPException(status_code=400, detail=str(e))
except KeyError as e:
@@ -533,6 +568,52 @@ def fastapi_config_get_key(
raise HTTPException(status_code=400, detail=str(e))
@app.get("/v1/logging/log", tags=["logging"])
async def fastapi_logging_get_log(
limit: int = Query(100, description="Maximum number of log entries to return."),
level: Optional[str] = Query(None, description="Filter by log level (e.g., INFO, ERROR)."),
contains: Optional[str] = Query(None, description="Filter logs containing this substring."),
regex: Optional[str] = Query(None, description="Filter logs by matching regex in message."),
from_time: Optional[str] = Query(
None, description="Start time (ISO format) for filtering logs."
),
to_time: Optional[str] = Query(None, description="End time (ISO format) for filtering logs."),
tail: bool = Query(False, description="If True, returns the most recent lines (tail mode)."),
) -> JSONResponse:
"""Get structured log entries from the EOS log file.
Filters and returns log entries based on the specified query parameters. The log
file is expected to contain newline-delimited JSON entries.
Args:
limit (int): Maximum number of entries to return.
level (Optional[str]): Filter logs by severity level (e.g., DEBUG, INFO).
contains (Optional[str]): Return only logs that include this string in the message.
regex (Optional[str]): Return logs that match this regular expression in the message.
from_time (Optional[str]): ISO 8601 timestamp to filter logs not older than this.
to_time (Optional[str]): ISO 8601 timestamp to filter logs not newer than this.
tail (bool): If True, fetch the most recent log entries (like `tail`).
Returns:
JSONResponse: A JSON list of log entries.
"""
log_path = config_eos.logging.file_path
try:
logs = read_file_log(
log_path=log_path,
limit=limit,
level=level,
contains=contains,
regex=regex,
from_time=from_time,
to_time=to_time,
tail=tail,
)
return JSONResponse(content=logs)
except Exception as e:
return JSONResponse(content={"error": str(e)}, status_code=500)
@app.get("/v1/measurement/keys", tags=["measurement"])
def fastapi_measurement_keys_get() -> list[str]:
"""Get a list of available measurement keys."""
@@ -848,7 +929,7 @@ def fastapi_prediction_update(
trace = "".join(traceback.TracebackException.from_exception(e).format())
raise HTTPException(
status_code=400,
detail=f"Error on prediction update: {e}{trace}",
detail=f"Error on prediction update: {e}\n{trace}",
)
return Response()
@@ -1145,9 +1226,14 @@ def fastapi_optimize(
# Perform optimization simulation
opt_class = optimization_problem(verbose=bool(config_eos.server.verbose))
result = opt_class.optimierung_ems(parameters=parameters, start_hour=start_hour, **extra_args)
# print(result)
return result
try:
result = opt_class.optimierung_ems(
parameters=parameters, start_hour=start_hour, **extra_args
)
# print(result)
return result
except Exception as e:
raise HTTPException(status_code=400, detail=f"Optimize error: {e}.")
@app.get("/visualization_results.pdf", response_class=PdfResponse, tags=["optimize"])
@@ -1201,7 +1287,7 @@ def redirect(request: Request, path: str) -> Union[HTMLResponse, RedirectRespons
if port is None:
port = 8504
# Make hostname Windows friendly
if host == "0.0.0.0" and os.name == "nt":
if host == "0.0.0.0" and os.name == "nt": # noqa: S104
host = "localhost"
url = f"http://{host}:{port}/"
error_page = create_error_page(
@@ -1218,7 +1304,7 @@ Did you want to connect to <a href="{url}" class="back-button">EOSdash</a>?
# Make hostname Windows friendly
host = str(config_eos.server.eosdash_host)
if host == "0.0.0.0" and os.name == "nt":
if host == "0.0.0.0" and os.name == "nt": # noqa: S104
host = "localhost"
if host and config_eos.server.eosdash_port:
# Redirect to EOSdash server
@@ -1229,45 +1315,46 @@ Did you want to connect to <a href="{url}" class="back-button">EOSdash</a>?
return RedirectResponse(url="/docs", status_code=303)
def run_eos(host: str, port: int, log_level: str, access_log: bool, reload: bool) -> None:
def run_eos(host: str, port: int, log_level: str, reload: bool) -> None:
"""Run the EOS server with the specified configurations.
This function starts the EOS server using the Uvicorn ASGI server. It accepts
arguments for the host, port, log level, access log, and reload options. The
log level is converted to lowercase to ensure compatibility with Uvicorn's
expected log level format. If an error occurs while attempting to bind the
server to the specified host and port, an error message is logged and the
application exits.
Starts the EOS server using the Uvicorn ASGI server. Logs an error and exits if
binding to the host and port fails.
Parameters:
host (str): The hostname to bind the server to.
port (int): The port number to bind the server to.
log_level (str): The log level for the server. Options include "critical", "error",
"warning", "info", "debug", and "trace".
access_log (bool): Whether to enable or disable the access log. Set to True to enable.
reload (bool): Whether to enable or disable auto-reload. Set to True for development.
Args:
host (str): Hostname to bind the server to.
port (int): Port number to bind the server to.
log_level (str): Log level for the server. One of
"critical", "error", "warning", "info", "debug", or "trace".
reload (bool): Enable or disable auto-reload. True for development.
Returns:
None
None
"""
# Make hostname Windows friendly
if host == "0.0.0.0" and os.name == "nt":
if host == "0.0.0.0" and os.name == "nt": # noqa: S104
host = "localhost"
# Setup console logging level using nested value
# - triggers logging configuration by track_logging_config
if log_level.upper() in LOGGING_LEVELS:
config_eos.set_nested_value("logging/console_level", log_level.upper())
# Wait for EOS port to be free - e.g. in case of restart
wait_for_port_free(port, timeout=120, waiting_app_name="EOS")
try:
# Let uvicorn run the fastAPI app
uvicorn.run(
"akkudoktoreos.server.eos:app",
host=host,
port=port,
log_level=log_level.lower(), # Convert log_level to lowercase
access_log=access_log,
log_level="info",
access_log=True,
reload=reload,
)
except Exception as e:
logger.error(f"Could not bind to host {host}:{port}. Error: {e}")
logger.exception("Failed to start uvicorn server.")
raise e
@@ -1282,7 +1369,7 @@ def main() -> None:
Command-line Arguments:
--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").
--log_level (str): Log level for the server console. 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).
"""
@@ -1306,14 +1393,8 @@ def main() -> None:
parser.add_argument(
"--log_level",
type=str,
default="info",
help='Log level for the server. Options: "critical", "error", "warning", "info", "debug", "trace" (default: "info")',
)
parser.add_argument(
"--access_log",
type=bool,
default=False,
help="Enable or disable access log. Options: True or False (default: True)",
default="none",
help='Log level for the server console. Options: "critical", "error", "warning", "info", "debug", "trace" (default: "none")',
)
parser.add_argument(
"--reload",
@@ -1328,7 +1409,7 @@ def main() -> None:
port = args.port if args.port else 8503
try:
run_eos(host, port, args.log_level, args.access_log, args.reload)
run_eos(host, port, args.log_level, args.reload)
except:
sys.exit(1)

View File

@@ -8,23 +8,21 @@ from typing import Optional
import psutil
import uvicorn
from fasthtml.common import FileResponse, JSONResponse
from loguru import logger
from monsterui.core import FastHTML, Theme
from akkudoktoreos.config.config import get_config
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.server.dash.bokeh import BokehJS
from akkudoktoreos.server.dash.components import Page
# Pages
from akkudoktoreos.server.dash.configuration import Configuration
from akkudoktoreos.server.dash.admin import Admin
from akkudoktoreos.server.dash.bokeh import BokehJS
from akkudoktoreos.server.dash.components import Page
from akkudoktoreos.server.dash.configuration import ConfigKeyUpdate, Configuration
from akkudoktoreos.server.dash.demo import Demo
from akkudoktoreos.server.dash.footer import Footer
from akkudoktoreos.server.dash.hello import Hello
from akkudoktoreos.server.server import get_default_host, wait_for_port_free
# from akkudoktoreos.server.dash.altair import AltairJS
logger = get_logger(__name__)
config_eos = get_config()
# The favicon for EOSdash
@@ -37,8 +35,7 @@ args: Optional[argparse.Namespace] = None
# Get frankenui and tailwind headers via CDN using Theme.green.headers()
# Add altair headers
# hdrs=(Theme.green.headers(highlightjs=True), AltairJS,)
# Add Bokeh headers
hdrs = (
Theme.green.headers(highlightjs=True),
BokehJS,
@@ -94,6 +91,7 @@ def get_eosdash(): # type: ignore
"EOSdash": "/eosdash/hello",
"Config": "/eosdash/configuration",
"Demo": "/eosdash/demo",
"Admin": "/eosdash/admin",
},
Hello(),
Footer(*eos_server()),
@@ -121,6 +119,21 @@ def get_eosdash_hello(): # type: ignore
return Hello()
@app.get("/eosdash/admin")
def get_eosdash_admin(): # type: ignore
"""Serves the EOSdash Admin page.
Returns:
Admin: The Admin page component.
"""
return Admin(*eos_server())
@app.post("/eosdash/admin")
def post_eosdash_admin(data: dict): # type: ignore
return Admin(*eos_server(), data)
@app.get("/eosdash/configuration")
def get_eosdash_configuration(): # type: ignore
"""Serves the EOSdash Configuration page.
@@ -131,6 +144,11 @@ def get_eosdash_configuration(): # type: ignore
return Configuration(*eos_server())
@app.put("/eosdash/configuration")
def put_eosdash_configuration(data: dict): # type: ignore
return ConfigKeyUpdate(*eos_server(), data["key"], data["value"])
@app.get("/eosdash/demo")
def get_eosdash_demo(): # type: ignore
"""Serves the EOSdash Demo page.
@@ -223,7 +241,7 @@ def run_eosdash() -> None:
reload = False
# Make hostname Windows friendly
if eosdash_host == "0.0.0.0" and os.name == "nt":
if eosdash_host == "0.0.0.0" and os.name == "nt": # noqa: S104
eosdash_host = "localhost"
# Wait for EOSdash port to be free - e.g. in case of restart

View File

@@ -1,3 +1,5 @@
import html
ERROR_PAGE_TEMPLATE = """
<!DOCTYPE html>
<html lang="en">
@@ -86,6 +88,6 @@ def create_error_page(
return (
ERROR_PAGE_TEMPLATE.replace("STATUS_CODE", status_code)
.replace("ERROR_TITLE", error_title)
.replace("ERROR_MESSAGE", error_message)
.replace("ERROR_DETAILS", error_details)
.replace("ERROR_MESSAGE", html.escape(error_message))
.replace("ERROR_DETAILS", html.escape(error_details))
)

View File

@@ -1,22 +1,51 @@
"""Server Module."""
import os
import ipaddress
import re
import time
from typing import Optional, Union
import psutil
from loguru import logger
from pydantic import Field, IPvAnyAddress, field_validator
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.logging import get_logger
logger = get_logger(__name__)
def get_default_host() -> str:
if os.name == "nt":
return "127.0.0.1"
return "0.0.0.0"
"""Default host for EOS."""
return "127.0.0.1"
def is_valid_ip_or_hostname(value: str) -> bool:
"""Validate whether a string is a valid IP address (IPv4 or IPv6) or hostname.
This function first attempts to interpret the input as an IP address using the
standard library `ipaddress` module. If that fails, it checks whether the input
is a valid hostname according to RFC 1123, which allows domain names consisting
of alphanumeric characters and hyphens, with specific length and structure rules.
Args:
value (str): The input string to validate.
Returns:
bool: True if the input is a valid IP address or hostname, False otherwise.
"""
try:
ipaddress.ip_address(value)
return True
except ValueError:
pass
if len(value) > 253:
return False
hostname_regex = re.compile(
r"^(?=.{1,253}$)(?!-)[A-Z\d-]{1,63}(?<!-)"
r"(?:\.(?!-)[A-Z\d-]{1,63}(?<!-))*\.?$",
re.IGNORECASE,
)
return bool(hostname_regex.fullmatch(value))
def wait_for_port_free(port: int, timeout: int = 0, waiting_app_name: str = "App") -> bool:
@@ -110,6 +139,8 @@ class ServerCommonSettings(SettingsBaseModel):
cls, value: Optional[Union[str, IPvAnyAddress]]
) -> Optional[Union[str, IPvAnyAddress]]:
if isinstance(value, str):
if not is_valid_ip_or_hostname(value):
raise ValueError(f"Invalid host: {value}")
if value.lower() in ("localhost", "loopback"):
value = "127.0.0.1"
return value

View File

@@ -27,13 +27,12 @@ from datetime import date, datetime, timedelta
from typing import Any, List, Literal, Optional, Tuple, Union, overload
import pendulum
from loguru import logger
from pendulum import Date, DateTime, Duration
from pendulum.tz.timezone import Timezone
from timezonefinder import TimezoneFinder
from akkudoktoreos.core.logging import get_logger
logger = get_logger(__name__)
MAX_DURATION_STRING_LENGTH = 350
@overload
@@ -153,22 +152,22 @@ def to_datetime(
fmt_tz = f"{fmt} z"
dt_tz = f"{date_input} {in_timezone}"
dt = pendulum.from_format(dt_tz, fmt_tz)
logger.debug(
logger.trace(
f"Str Fmt converted: {dt}, tz={dt.tz} from {date_input}, tz={in_timezone}"
)
break
except ValueError as e:
logger.debug(f"{date_input}, {fmt}, {e}")
logger.trace(f"{date_input}, {fmt}, {e}")
dt = None
else:
# DateTime input with timezone info
try:
dt = pendulum.parse(date_input)
logger.debug(
logger.trace(
f"Pendulum Fmt converted: {dt}, tz={dt.tz} from {date_input}, tz={in_timezone}"
)
except pendulum.parsing.exceptions.ParserError as e:
logger.debug(f"Date string {date_input} does not match any Pendulum formats: {e}")
logger.trace(f"Date string {date_input} does not match any Pendulum formats: {e}")
dt = None
if dt is None:
# Some special values
@@ -180,7 +179,7 @@ def to_datetime(
timestamp = float(date_input)
dt = pendulum.from_timestamp(timestamp, tz="UTC")
except (ValueError, TypeError) as e:
logger.debug(f"Date string {date_input} does not match timestamp format: {e}")
logger.trace(f"Date string {date_input} does not match timestamp format: {e}")
dt = None
if dt is None:
raise ValueError(f"Date string {date_input} does not match any known formats.")
@@ -201,7 +200,7 @@ def to_datetime(
# Represent in target timezone
dt_in_tz = dt.in_timezone(in_timezone)
logger.debug(
logger.trace(
f"\nTimezone adapted to: {in_timezone}\nfrom: {dt} tz={dt.timezone}\nto: {dt_in_tz} tz={dt_in_tz.tz}"
)
dt = dt_in_tz
@@ -276,7 +275,7 @@ def to_duration(
duration = pendulum.parse(input_value)
return duration - duration.start_of("day")
except pendulum.parsing.exceptions.ParserError as e:
logger.debug(f"Invalid Pendulum time string format '{input_value}': {e}")
logger.trace(f"Invalid Pendulum time string format '{input_value}': {e}")
# Handle strings like "2 days 5 hours 30 minutes"
total_seconds = 0
@@ -287,6 +286,11 @@ def to_duration(
"second": 1,
}
# Mitigate ReDoS vulnerability (#494) by checking input string length.
if len(input_value) > MAX_DURATION_STRING_LENGTH:
raise ValueError(
f"Input string exceeds maximum allowed length ({MAX_DURATION_STRING_LENGTH})."
)
# Regular expression to match time components like '2 days', '5 hours', etc.
matches = re.findall(r"(\d+)\s*(days?|hours?|minutes?|seconds?)", input_value)

View File

@@ -35,6 +35,8 @@ def get_model_structure_from_examples(
example_data: list[dict[str, Any]] = [{} for _ in range(example_max_length)]
for field_name, field_info in model_class.model_fields.items():
if field_info.deprecated:
continue
for example_ix in range(example_max_length):
example_data[example_ix][field_name] = get_example_or_default(
field_name, field_info, example_ix

View File

@@ -4,9 +4,6 @@ from typing import Any
import numpy as np
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.logging import get_logger
logger = get_logger(__name__)
class UtilsCommonSettings(SettingsBaseModel):

View File

@@ -12,16 +12,18 @@ import pendulum
from matplotlib.backends.backend_pdf import PdfPages
from akkudoktoreos.core.coreabc import ConfigMixin
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.core.ems import EnergyManagement
from akkudoktoreos.optimization.genetic import OptimizationParameters
from akkudoktoreos.utils.datetimeutil import to_datetime
logger = get_logger(__name__)
matplotlib.use(
"Agg"
) # non-interactive backend that can only write to files, backend needed to stay in main thread.
debug_visualize: bool = False
class VisualizationReport(ConfigMixin):
def __init__(
self,
@@ -162,14 +164,18 @@ class VisualizationReport(ConfigMixin):
# Format the time axis
plt.gca().xaxis.set_major_formatter(
mdates.DateFormatter("%Y-%m-%d")
mdates.DateFormatter("%Y-%m-%d", tz=self.config.general.timezone)
) # Show date and time
plt.gca().xaxis.set_major_locator(
mdates.DayLocator(interval=1, tz=None)
mdates.DayLocator(interval=1, tz=self.config.general.timezone)
) # Major ticks every day
plt.gca().xaxis.set_minor_locator(mdates.HourLocator(interval=3, tz=None))
plt.gca().xaxis.set_minor_locator(
mdates.HourLocator(interval=2, tz=self.config.general.timezone)
)
# Minor ticks every 6 hours
plt.gca().xaxis.set_minor_formatter(mdates.DateFormatter("%H"))
plt.gca().xaxis.set_minor_formatter(
mdates.DateFormatter("%H", tz=self.config.general.timezone)
)
# plt.gcf().autofmt_xdate(rotation=45, which="major")
# Auto-format the x-axis for readability
@@ -202,8 +208,10 @@ class VisualizationReport(ConfigMixin):
hours_since_start = [(t - timestamps[0]).total_seconds() / 3600 for t in timestamps]
# ax2.set_xticks(timestamps[::48]) # Set ticks every 12 hours
# ax2.set_xticklabels([f"{int(h)}" for h in hours_since_start[::48]])
ax2.set_xticks(timestamps[:: len(timestamps) // 24]) # Select 10 evenly spaced ticks
ax2.set_xticklabels([f"{int(h)}" for h in hours_since_start[:: len(timestamps) // 24]])
# ax2.set_xticks(timestamps[:: len(timestamps) // 24]) # Select 10 evenly spaced ticks
ax2.set_xticks(timestamps[:: len(timestamps) // 12]) # Select 10 evenly spaced ticks
# ax2.set_xticklabels([f"{int(h)}" for h in hours_since_start[:: len(timestamps) // 24]])
ax2.set_xticklabels([f"{int(h)}" for h in hours_since_start[:: len(timestamps) // 12]])
if x2label:
ax2.set_xlabel(x2label)
@@ -431,15 +439,17 @@ def prepare_visualize(
parameters: OptimizationParameters,
results: dict,
filename: str = "visualization_results.pdf",
start_hour: Optional[int] = 0,
start_hour: int = 0,
) -> None:
global debug_visualize
report = VisualizationReport(filename)
next_full_hour_date = pendulum.now(report.config.general.timezone).start_of("hour").add(hours=1)
next_full_hour_date = EnergyManagement.set_start_datetime()
# Group 1:
report.create_line_chart_date(
next_full_hour_date, # start_date
next_full_hour_date,
[
parameters.ems.gesamtlast,
parameters.ems.gesamtlast[start_hour:],
],
title="Load Profile",
# xlabel="Hours", # not enough space
@@ -447,9 +457,9 @@ def prepare_visualize(
labels=["Total Load (Wh)"],
)
report.create_line_chart_date(
next_full_hour_date, # start_date
next_full_hour_date,
[
parameters.ems.pv_prognose_wh,
parameters.ems.pv_prognose_wh[start_hour:],
],
title="PV Forecast",
# xlabel="Hours", # not enough space
@@ -457,8 +467,15 @@ def prepare_visualize(
)
report.create_line_chart_date(
next_full_hour_date, # start_date
[np.full(len(parameters.ems.gesamtlast), parameters.ems.einspeiseverguetung_euro_pro_wh)],
next_full_hour_date,
[
np.full(
len(parameters.ems.gesamtlast) - start_hour,
parameters.ems.einspeiseverguetung_euro_pro_wh[start_hour:]
if isinstance(parameters.ems.einspeiseverguetung_euro_pro_wh, list)
else parameters.ems.einspeiseverguetung_euro_pro_wh,
)
],
title="Remuneration",
# xlabel="Hours", # not enough space
ylabel="€/Wh",
@@ -466,9 +483,9 @@ def prepare_visualize(
)
if parameters.temperature_forecast:
report.create_line_chart_date(
next_full_hour_date, # start_date
next_full_hour_date,
[
parameters.temperature_forecast,
parameters.temperature_forecast[start_hour:],
],
title="Temperature Forecast",
# xlabel="Hours", # not enough space
@@ -517,21 +534,35 @@ def prepare_visualize(
)
report.create_line_chart_date(
next_full_hour_date, # start_date
[parameters.ems.strompreis_euro_pro_wh],
title="Electricity Price",
[parameters.ems.strompreis_euro_pro_wh[start_hour:]],
# title="Electricity Price", # not enough space
# xlabel="Date", # not enough space
ylabel="Electricity Price (€/Wh)",
x2label=None, # not enough space
)
labels = list(
item
for sublist in zip(
list(str(i) for i in range(0, 23, 2)), list(str(" ") for i in range(0, 23, 2))
)
for item in sublist
)
labels = labels[start_hour:] + labels
report.create_bar_chart(
list(str(i) for i in range(len(results["ac_charge"]))),
[results["ac_charge"], results["dc_charge"], results["discharge_allowed"]],
labels,
[
results["ac_charge"][start_hour:],
results["dc_charge"][start_hour:],
results["discharge_allowed"][start_hour:],
],
title="AC/DC Charging and Discharge Overview",
ylabel="Relative Power (0-1) / Discharge (0 or 1)",
label_names=["AC Charging (relative)", "DC Charging (relative)", "Discharge Allowed"],
colors=["blue", "green", "red"],
bottom=3,
xlabels=labels,
)
report.finalize_group()
@@ -614,7 +645,7 @@ def prepare_visualize(
if filtered_balance.size > 0 or filtered_losses.size > 0:
report.finalize_group()
if logger.level == "DEBUG" or results["fixed_seed"]:
if debug_visualize or results["fixed_seed"]:
report.create_line_chart(
0,
[
@@ -639,6 +670,8 @@ def prepare_visualize(
def generate_example_report(filename: str = "example_report.pdf") -> None:
"""Generate example visualization report."""
global debug_visualize
report = VisualizationReport(filename, "test")
x_hours = 0 # Define x-axis start values (e.g., hours)
@@ -710,9 +743,9 @@ def generate_example_report(filename: str = "example_report.pdf") -> None:
report.finalize_group() # Finalize the third group of charts
logger.setLevel("DEBUG") # set level for example report
debug_visualize = True # set level for example report
if logger.level == "DEBUG":
if debug_visualize:
report.create_line_chart(
x_hours,
[np.array([0.2, 0.25, 0.3, 0.35])],

View File

@@ -16,35 +16,75 @@ import pendulum
import psutil
import pytest
import requests
from _pytest.logging import LogCaptureFixture
from loguru import logger
from xprocess import ProcessStarter, XProcess
from akkudoktoreos.config.config import ConfigEOS, get_config
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.server.server import get_default_host
logger = get_logger(__name__)
# -----------------------------------------------
# Adapt pytest logging handling to Loguru logging
# -----------------------------------------------
@pytest.fixture
def caplog(caplog: LogCaptureFixture):
"""Propagate Loguru logs to the pytest caplog handler."""
handler_id = logger.add(
caplog.handler,
format="{message}",
level=0,
filter=lambda record: record["level"].no >= caplog.handler.level,
enqueue=False, # Set to 'True' if your test is spawning child processes.
)
yield caplog
try:
logger.remove(handler_id)
except:
# May already be deleted
pass
@pytest.fixture
def reportlog(pytestconfig):
"""Propagate Loguru logs to the pytest terminal reporter."""
logging_plugin = pytestconfig.pluginmanager.getplugin("logging-plugin")
handler_id = logger.add(logging_plugin.report_handler, format="{message}")
yield
try:
logger.remove(handler_id)
except:
# May already be deleted
pass
@pytest.fixture(autouse=True)
def propagate_logs():
"""Deal with the pytest --log-cli-level command-line flag.
This option controls the standard logging logs, not loguru ones.
For this reason, we first install a PropagateHandler for compatibility.
"""
class PropagateHandler(logging.Handler):
def emit(self, record):
if logging.getLogger(record.name).isEnabledFor(record.levelno):
logging.getLogger(record.name).handle(record)
logger.remove()
logger.add(PropagateHandler(), format="{message}")
yield
@pytest.fixture()
def disable_debug_logging(scope="session", autouse=True):
"""Automatically disable debug logging for all tests."""
original_levels = {}
root_logger = logging.getLogger()
logger.remove() # Remove all loggers
logger.add(sys.stderr, level="INFO") # Only show INFO and above
original_levels[root_logger] = root_logger.level
root_logger.setLevel(logging.INFO)
for logger_name, logger in logging.root.manager.loggerDict.items():
if isinstance(logger, logging.Logger):
original_levels[logger] = logger.level
if logger.level <= logging.DEBUG:
logger.setLevel(logging.INFO)
yield
for logger, level in original_levels.items():
logger.setLevel(level)
# -----------------------------------------------
# Provide pytest options for specific test setups
# -----------------------------------------------
def pytest_addoption(parser):
parser.addoption(
@@ -144,6 +184,7 @@ def cfg_non_existent(request):
@pytest.fixture(autouse=True)
def user_cwd(config_default_dirs):
"""Patch cwd provided by module pathlib.Path.cwd."""
with patch(
"pathlib.Path.cwd",
return_value=config_default_dirs[1],
@@ -153,6 +194,7 @@ def user_cwd(config_default_dirs):
@pytest.fixture(autouse=True)
def user_config_dir(config_default_dirs):
"""Patch user_config_dir provided by module platformdirs."""
with patch(
"akkudoktoreos.config.config.user_config_dir",
return_value=str(config_default_dirs[0]),
@@ -162,6 +204,7 @@ def user_config_dir(config_default_dirs):
@pytest.fixture(autouse=True)
def user_data_dir(config_default_dirs):
"""Patch user_data_dir provided by module platformdirs."""
with patch(
"akkudoktoreos.config.config.user_data_dir",
return_value=str(config_default_dirs[-1] / "data"),
@@ -189,14 +232,18 @@ def config_eos(
config_file_cwd = config_default_dirs[1] / ConfigEOS.CONFIG_FILE_NAME
assert not config_file.exists()
assert not config_file_cwd.exists()
config_eos = get_config()
config_eos.reset_settings()
assert config_file == config_eos.general.config_file_path
assert config_file.exists()
assert not config_file_cwd.exists()
# Check user data directory pathes (config_default_dirs[-1] == data_default_dir_user)
assert config_default_dirs[-1] / "data" == config_eos.general.data_folder_path
assert config_default_dirs[-1] / "data/cache" == config_eos.cache.path()
assert config_default_dirs[-1] / "data/output" == config_eos.general.data_output_path
assert config_default_dirs[-1] / "data/output/eos.log" == config_eos.logging.file_path
return config_eos
@@ -429,7 +476,7 @@ def server(xprocess, config_eos, config_default_dirs) -> Generator[str, None, No
Provides URL of the server.
"""
# create url/port info to the server
url = "http://0.0.0.0:8503"
url = "http://127.0.0.1:8503"
class Starter(ProcessStarter):
# Set environment before any subprocess run, to keep custom config dir

View File

@@ -4,12 +4,10 @@ from typing import Union
from unittest.mock import patch
import pytest
from loguru import logger
from pydantic import ValidationError
from akkudoktoreos.config.config import ConfigEOS, GeneralSettings
from akkudoktoreos.core.logging import get_logger
logger = get_logger(__name__)
# overwrite config_mixin fixture from conftest
@@ -304,7 +302,7 @@ def test_config_common_settings_timezone_none_when_coordinates_missing():
[0.0, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0],
)
],
ValueError,
TypeError,
),
# Invalid index (out of bound)
(
@@ -316,7 +314,7 @@ def test_config_common_settings_timezone_none_when_coordinates_missing():
[0.0, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0],
)
],
IndexError,
TypeError,
),
# Invalid index (no number)
(
@@ -346,14 +344,27 @@ def test_config_common_settings_timezone_none_when_coordinates_missing():
)
def test_set_nested_key(path, value, expected, exception, config_eos):
if not exception:
config_eos.set_config_value(path, value)
config_eos.set_nested_value(path, value)
for expected_path, expected_value in expected:
assert eval(f"config_eos.{expected_path}") == expected_value
actual_value = eval(f"config_eos.{expected_path}")
assert actual_value == expected_value, (
f"Expected {expected_value} at {expected_path}, but got {actual_value}"
)
else:
with pytest.raises(exception):
config_eos.set_config_value(path, value)
for expected_path, expected_value in expected:
assert eval(f"config_eos.{expected_path}") == expected_value
try:
config_eos.set_nested_value(path, value)
for expected_path, expected_value in expected:
actual_value = eval(f"config_eos.{expected_path}")
assert actual_value == expected_value, (
f"Expected {expected_value} at {expected_path}, but got {actual_value}"
)
pytest.fail(
f"Expected exception {exception} but none was raised. Set '{expected_path}' to '{actual_value}'"
)
except Exception as e:
assert isinstance(e, exception), (
f"Expected exception {exception}, but got {type(e)}: {e}"
)
@pytest.mark.parametrize(
@@ -378,10 +389,10 @@ def test_set_nested_key(path, value, expected, exception, config_eos):
)
def test_get_nested_key(path, expected_value, exception, config_eos):
if not exception:
assert config_eos.get_config_value(path) == expected_value
assert config_eos.get_nested_value(path) == expected_value
else:
with pytest.raises(exception):
config_eos.get_config_value(path)
config_eos.get_nested_value(path)
def test_merge_settings_from_dict_invalid(config_eos):

View File

@@ -156,7 +156,7 @@ class TestDataRecord:
assert "data_value" in record_dict
assert record_dict["data_value"] == 20.0
record2 = DerivedRecord.from_dict(record_dict)
assert record2 == record
assert record2.model_dump() == record.model_dump()
def test_to_json(self):
record = self.create_test_record(datetime(2024, 1, 3, tzinfo=timezone.utc), 10.0)
@@ -165,7 +165,7 @@ class TestDataRecord:
assert "data_value" in json_str
assert "20.0" in json_str
record2 = DerivedRecord.from_json(json_str)
assert record2 == record
assert record2.model_dump() == record.model_dump()
class TestDataSequence:
@@ -526,7 +526,7 @@ class TestDataSequence:
data_dict = sequence.to_dict()
assert isinstance(data_dict, dict)
sequence_other = sequence.from_dict(data_dict)
assert sequence_other == sequence
assert sequence_other.model_dump() == sequence.model_dump()
def test_to_json(self, sequence):
record = self.create_test_record(datetime(2023, 11, 6), 0.8)

View File

@@ -1,10 +1,13 @@
"""Test Module for pendulum.datetimeutil Module."""
import re
import pendulum
import pytest
from pendulum.tz.timezone import Timezone
from akkudoktoreos.utils.datetimeutil import (
MAX_DURATION_STRING_LENGTH,
compare_datetimes,
hours_in_day,
to_datetime,
@@ -368,7 +371,7 @@ def test_to_datetime(
# print(f"Result: {result} tz={result.timezone}")
# print(f"Compare: {compare}")
if expected_approximately:
assert compare.time_diff < 200
assert compare.time_diff < 300
else:
assert compare.equal == True
@@ -620,3 +623,33 @@ def test_compare_datetimes_gt(dt1, dt2):
assert compare_datetimes(dt1, dt2).gt
assert compare_datetimes(dt1, dt2).le == False
assert compare_datetimes(dt1, dt2).lt == False
def test_to_duration_excessive_length_raises_valueerror():
"""Test that to_duration raises ValueError for strings exceeding max length.
This test covers the fix for the ReDoS vulnerability.
Related to: #494
"""
# String exceeds limits
long_string = "a" * (MAX_DURATION_STRING_LENGTH + 50)
# Expected Errormessage ESCAPED für Regex
expected_error_message = re.escape(
f"Input string exceeds maximum allowed length ({MAX_DURATION_STRING_LENGTH})."
)
# Check if error was raised
with pytest.raises(ValueError, match=expected_error_message):
to_duration(long_string)
# Optional: String exactly at the limit should NOT trigger the length check.
at_limit_string = "b" * MAX_DURATION_STRING_LENGTH
try:
to_duration(at_limit_string)
except ValueError as e:
if str(e) == f"Input string exceeds maximum allowed length ({MAX_DURATION_STRING_LENGTH}).":
pytest.fail(
f"to_duration raised length ValueError unexpectedly for string at limit: {at_limit_string}"
)
pass

View File

@@ -28,8 +28,6 @@ def test_openapi_spec_current(config_eos):
spec = generate_openapi.generate_openapi()
spec_str = json.dumps(spec, indent=4, sort_keys=True)
if os.name == "nt":
spec_str = spec_str.replace("127.0.0.1", "0.0.0.0")
with new_spec_path.open("w", encoding="utf-8", newline="\n") as f_new:
f_new.write(spec_str)
@@ -62,8 +60,6 @@ def test_openapi_md_current(config_eos):
spec_md = generate_openapi_md.generate_openapi_md()
if os.name == "nt":
spec_md = spec_md.replace("127.0.0.1", "0.0.0.0")
with new_spec_md_path.open("w", encoding="utf-8", newline="\n") as f_new:
f_new.write(spec_md)
@@ -94,7 +90,7 @@ def test_config_md_current(config_eos):
config_md = generate_config_md.generate_config_md(config_eos)
if os.name == "nt":
config_md = config_md.replace("127.0.0.1", "0.0.0.0").replace("\\\\", "/")
config_md = config_md.replace("\\\\", "/")
with new_config_md_path.open("w", encoding="utf-8", newline="\n") as f_new:
f_new.write(config_md)

View File

@@ -5,10 +5,10 @@ from unittest.mock import Mock, patch
import numpy as np
import pytest
import requests
from loguru import logger
from akkudoktoreos.core.cache import CacheFileStore
from akkudoktoreos.core.ems import get_ems
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.prediction.elecpriceakkudoktor import (
AkkudoktorElecPrice,
AkkudoktorElecPriceValue,
@@ -22,8 +22,6 @@ FILE_TESTDATA_ELECPRICEAKKUDOKTOR_1_JSON = DIR_TESTDATA.joinpath(
"elecpriceforecast_akkudoktor_1.json"
)
logger = get_logger(__name__)
@pytest.fixture
def provider(monkeypatch, config_eos):
@@ -94,7 +92,7 @@ def test_request_forecast(mock_get, provider, sample_akkudoktor_1_json):
akkudoktor_data = provider._request_forecast()
assert isinstance(akkudoktor_data, AkkudoktorElecPrice)
assert akkudoktor_data.values[0] == AkkudoktorElecPriceValue(
assert akkudoktor_data.values[0].model_dump() == AkkudoktorElecPriceValue(
start_timestamp=1733785200000,
end_timestamp=1733788800000,
start="2024-12-09T23:00:00.000Z",
@@ -102,7 +100,7 @@ def test_request_forecast(mock_get, provider, sample_akkudoktor_1_json):
marketprice=92.85,
unit="Eur/MWh",
marketpriceEurocentPerKWh=9.29,
)
).model_dump()
@patch("requests.get")

View File

@@ -0,0 +1,208 @@
import json
from pathlib import Path
from unittest.mock import Mock, patch
import numpy as np
import pytest
import requests
from loguru import logger
from akkudoktoreos.core.cache import CacheFileStore
from akkudoktoreos.core.ems import get_ems
from akkudoktoreos.prediction.elecpriceakkudoktor import (
AkkudoktorElecPrice,
AkkudoktorElecPriceValue,
ElecPriceAkkudoktor,
)
from akkudoktoreos.prediction.elecpriceenergycharts import (
ElecPriceEnergyCharts,
EnergyChartsElecPrice,
)
from akkudoktoreos.utils.datetimeutil import to_datetime
DIR_TESTDATA = Path(__file__).absolute().parent.joinpath("testdata")
FILE_TESTDATA_ELECPRICE_ENERGYCHARTS_JSON = DIR_TESTDATA.joinpath(
"elecpriceforecast_energycharts.json"
)
@pytest.fixture
def provider(monkeypatch, config_eos):
"""Fixture to create a ElecPriceProvider instance."""
monkeypatch.setenv("EOS_ELECPRICE__ELECPRICE_PROVIDER", "ElecPriceEnergyCharts")
config_eos.reset_settings()
return ElecPriceEnergyCharts()
@pytest.fixture
def sample_energycharts_json():
"""Fixture that returns sample forecast data report."""
with FILE_TESTDATA_ELECPRICE_ENERGYCHARTS_JSON.open(
"r", encoding="utf-8", newline=None
) as f_res:
input_data = json.load(f_res)
return input_data
@pytest.fixture
def cache_store():
"""A pytest fixture that creates a new CacheFileStore instance for testing."""
return CacheFileStore()
# ------------------------------------------------
# General forecast
# ------------------------------------------------
def test_singleton_instance(provider):
"""Test that ElecPriceForecast behaves as a singleton."""
another_instance = ElecPriceEnergyCharts()
assert provider is another_instance
def test_invalid_provider(provider, monkeypatch):
"""Test requesting an unsupported provider."""
monkeypatch.setenv("EOS_ELECPRICE__ELECPRICE_PROVIDER", "<invalid>")
provider.config.reset_settings()
assert not provider.enabled()
# ------------------------------------------------
# Akkudoktor
# ------------------------------------------------
@patch("akkudoktoreos.prediction.elecpriceenergycharts.logger.error")
def test_validate_data_invalid_format(mock_logger, provider):
"""Test validation for invalid Energy-Charts data."""
invalid_data = '{"invalid": "data"}'
with pytest.raises(ValueError):
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, provider, sample_energycharts_json):
"""Test requesting forecast from Energy-Charts."""
# Mock response object
mock_response = Mock()
mock_response.status_code = 200
mock_response.content = json.dumps(sample_energycharts_json)
mock_get.return_value = mock_response
# Test function
energy_charts_data = provider._request_forecast()
assert isinstance(energy_charts_data, EnergyChartsElecPrice)
assert energy_charts_data.unix_seconds[0] == 1733785200
assert energy_charts_data.price[0] == 92.85
@patch("requests.get")
def test_update_data(mock_get, provider, sample_energycharts_json, cache_store):
"""Test fetching forecast from Energy-Charts."""
# Mock response object
mock_response = Mock()
mock_response.status_code = 200
mock_response.content = json.dumps(sample_energycharts_json)
mock_get.return_value = mock_response
cache_store.clear(clear_all=True)
# Call the method
ems_eos = get_ems()
ems_eos.set_start_datetime(to_datetime("2024-12-11 00:00:00", in_timezone="Europe/Berlin"))
provider.update_data(force_enable=True, force_update=True)
# Assert: Verify the result is as expected
mock_get.assert_called_once()
assert (
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 hours prioce values by resampling
np_price_array = provider.key_to_array(
key="elecprice_marketprice_wh",
start_datetime=provider.start_datetime,
end_datetime=provider.end_datetime,
)
assert len(np_price_array) == provider.total_hours
@patch("requests.get")
def test_update_data_with_incomplete_forecast(mock_get, provider):
"""Test `_update_data` with incomplete or missing forecast data."""
incomplete_data: dict = {"license_info": "", "unix_seconds": [], "price": [], "unit": "", "deprecated": False}
mock_response = Mock()
mock_response.status_code = 200
mock_response.content = json.dumps(incomplete_data)
mock_get.return_value = mock_response
logger.info("The following errors are intentional and part of the test.")
with pytest.raises(ValueError):
provider._update_data(force_update=True)
@pytest.mark.parametrize(
"status_code, exception",
[(400, requests.exceptions.HTTPError), (500, requests.exceptions.HTTPError), (200, None)],
)
@patch("requests.get")
def test_request_forecast_status_codes(
mock_get, provider, sample_energycharts_json, status_code, exception
):
"""Test handling of various API status codes."""
mock_response = Mock()
mock_response.status_code = status_code
mock_response.content = json.dumps(sample_energycharts_json)
mock_response.raise_for_status.side_effect = (
requests.exceptions.HTTPError if exception else None
)
mock_get.return_value = mock_response
if exception:
with pytest.raises(exception):
provider._request_forecast()
else:
provider._request_forecast()
@patch("akkudoktoreos.core.cache.CacheFileStore")
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
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(provider):
"""Test resampling of forecast data to NumPy array."""
provider.update_data(force_update=True)
array = provider.key_to_array(
key="elecprice_marketprice_wh",
start_datetime=provider.start_datetime,
end_datetime=provider.end_datetime,
)
assert isinstance(array, np.ndarray)
assert len(array) == provider.total_hours
# ------------------------------------------------
# Development Akkudoktor
# ------------------------------------------------
@pytest.mark.skip(reason="For development only")
def test_akkudoktor_development_forecast_data(provider):
"""Fetch data from real Energy-Charts server."""
# Preset, as this is usually done by update_data()
provider.start_datetime = to_datetime("2024-10-26 00:00:00")
energy_charts_data = provider._request_forecast()
with FILE_TESTDATA_ELECPRICE_ENERGYCHARTS_JSON.open(
"w", encoding="utf-8", newline="\n"
) as f_out:
json.dump(energy_charts_data, f_out, indent=4)

View File

@@ -59,8 +59,8 @@ def test_invalid_provider(provider, config_eos):
},
}
}
config_eos.merge_settings_from_dict(settings)
assert not provider.enabled()
with pytest.raises(ValueError, match="not a valid electricity price provider"):
config_eos.merge_settings_from_dict(settings)
# ------------------------------------------------

105
tests/test_eosdashconfig.py Normal file
View File

@@ -0,0 +1,105 @@
"""Test suite for the EOS Dash configuration module.
This module contains tests for utility functions related to retrieving and processing
configuration data using Pydantic models.
"""
import json
from pathlib import Path
from typing import Union
import pytest
from pydantic.fields import FieldInfo
from akkudoktoreos.core.pydantic import PydanticBaseModel
from akkudoktoreos.prediction.pvforecast import PVForecastPlaneSetting
from akkudoktoreos.server.dash.configuration import (
configuration,
get_default_value,
get_nested_value,
resolve_nested_types,
)
DIR_TESTDATA = Path(__file__).absolute().parent.joinpath("testdata")
FILE_TESTDATA_EOSSERVER_CONFIG_1 = DIR_TESTDATA.joinpath("eosserver_config_1.json")
class SampleModel(PydanticBaseModel):
field1: str = "default_value"
field2: int = 10
class TestEOSdashConfig:
"""Test case for EOS Dash configuration utility functions.
This class tests functions for retrieving nested values, extracting default values,
resolving nested types, and generating configuration details from Pydantic models.
"""
def test_get_nested_value_from_dict(self):
"""Test retrieving a nested value from a dictionary using a sequence of keys."""
data = {"a": {"b": {"c": 42}}}
assert get_nested_value(data, ["a", "b", "c"]) == 42
assert get_nested_value(data, ["a", "x"], default="not found") == "not found"
with pytest.raises(TypeError):
get_nested_value("not_a_dict", ["a"]) # type: ignore
def test_get_nested_value_from_list(self):
"""Test retrieving a nested value from a list using a sequence of keys."""
data = {"a": {"b": {"c": [42]}}}
assert get_nested_value(data, ["a", "b", "c", 0]) == 42
assert get_nested_value(data, ["a", "b", "c", "0"]) == 42
def test_get_default_value(self):
"""Test retrieving the default value of a field based on FieldInfo metadata."""
field_info = FieldInfo(default="test_value")
assert get_default_value(field_info, True) == "test_value"
field_info_no_default = FieldInfo()
assert get_default_value(field_info_no_default, True) == ""
assert get_default_value(field_info, False) == "N/A"
def test_resolve_nested_types(self):
"""Test resolving nested types within a field, ensuring correct type extraction."""
nested_types = resolve_nested_types(Union[int, str], [])
assert (int, []) in nested_types
assert (str, []) in nested_types
def test_configuration(self):
"""Test extracting configuration details from a Pydantic model based on provided values."""
values = {"field1": "custom_value", "field2": 20}
config = configuration(SampleModel, values)
assert any(
item["name"] == "field1" and item["value"] == '"custom_value"' for item in config
)
assert any(item["name"] == "field2" and item["value"] == "20" for item in config)
def test_configuration_eos(self, config_eos):
"""Test extracting EOS configuration details from EOS config based on provided values."""
with FILE_TESTDATA_EOSSERVER_CONFIG_1.open("r", encoding="utf-8", newline=None) as fd:
values = json.load(fd)
config = configuration(config_eos, values)
assert any(
item["name"] == "server.eosdash_port" and item["value"] == "8504" for item in config
)
assert any(
item["name"] == "server.eosdash_host" and item["value"] == '"127.0.0.1"'
for item in config
)
def test_configuration_pvforecast_plane_settings(self):
"""Test extracting EOS PV forecast plane configuration details from EOS config based on provided values."""
with FILE_TESTDATA_EOSSERVER_CONFIG_1.open("r", encoding="utf-8", newline=None) as fd:
values = json.load(fd)
config = configuration(
PVForecastPlaneSetting(), values, values_prefix=["pvforecast", "planes", "0"]
)
assert any(
item["name"] == "pvforecast.planes.0.surface_azimuth" and item["value"] == "170"
for item in config
)
assert any(
item["name"] == "pvforecast.planes.0.userhorizon"
and item["value"] == "[20, 27, 22, 20]"
for item in config
)

Some files were not shown because too many files have changed in this diff Show More