Commit Graph
7 Commits
Author SHA1 Message Date
70b72bfbce feat(ems): notify a URL after every completed optimization (#1383)
* feat(devices): add load-dependent DC-to-AC efficiency curve for GENETIC inverters

Real inverters are much less efficient at low load than at rated power
(e.g. 0.80 at 2 % vs. 0.93 at 7 % load). A constant dc_to_ac_efficiency
overrates night-time battery discharge into a small base load and, with
it, arbitrage and grid charging.

Add two optional inverter settings for the GENETIC optimizer:

- dc_to_ac_efficiency_curve: list of (load_fraction, efficiency) points,
  load fraction = AC power / max_power_w. Points must be strictly
  increasing in [0, 1] with efficiencies in (0, 1]; at least two points.
  Linear interpolation, clamped at both ends. When set, the curve
  replaces dc_to_ac_efficiency.
- dc_to_ac_efficiency_reference_load_fraction (default 0.06): load at
  which the curve values stored energy without a specific conversion.

Battery discharges in Inverter.process_energy() use the efficiency at
the AC energy of each conversion. The end-of-horizon value, the terminal
and tail value curves and the AC charge break-even penalty use
Inverter.reference_dc_to_ac_efficiency.

Without a curve, dc_to_ac_efficiency_at() and the reference efficiency
return dc_to_ac_efficiency itself, so results are bit-identical to the
previous model. GENETIC0 is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(genetic): make the fitness cache size configurable

The per-run fitness cache of the GENETIC optimizer is unbounded. It
grows with individuals x generations (~110k keys at 300 x 400, about
1.1 kB each, 1.6 kB with an EV) and adds 120-170 MB to the peak memory
of a run, while only 6-15 % of the lookups hit. On 512 MB boards this
is the largest part of the optimizer's memory.

Add optimization.genetic.fitness_cache_max_entries:

- None (default): unbounded, the previous behaviour.
- 0: no cache; every genome is evaluated.
- N > 0: at most N keys. The cache is then an OrderedDict and evicts
  the oldest key first (FIFO, O(1)); LRU would cost bookkeeping on
  every hit for a cache that rarely hits.

A cache hit returns exactly what the deterministic evaluation would
return, so the plan for a fixed seed is identical with any setting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf: import heavy optional dependencies on first use

A GENETIC server with external (import) providers and EOSdash disabled
loads several large packages at start that it never uses, which costs
a noticeable part of the memory on 512 MB boards:

- matplotlib via the GENETIC/GENETIC0 PDF report modules (server/eos.py)
- statsmodels via the Holt-Winters fallback in prediction/priceabc.py
- pvlib via the weather base class and the pvlib-based providers
- bs4/lxml via the ClearOutside provider
- scipy via the self-consumption interpolator singletons, which were
  loaded at import time
- the GENETIC0 optimizer, imported by core/ems.py
- tzfpy, whose polygon data the computed general.timezone loads

Import these where they are used. The interpolator tables are loaded
on first use (thread-safe). Nothing changes in what is computed.

general.timezone is computed from latitude/longitude and cannot be set.
Add general.timezone_override (IANA name, validated with pendulum):
when set, general.timezone returns it and the lookup is skipped. When
unset (default) the lookup is unchanged.

Tests that patched the moved names now patch them at their source.
A new test starts the server module in a subprocess and checks that
none of these modules is loaded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(prediction): evaluate self-consumption tables without SciPy

The GENETIC inverter (and the GENETIC0 one) evaluates a pickled
scipy.interpolate.RegularGridInterpolator for the self-consumption
probabilities. SciPy is used nowhere else in EOS, yet loading it costs
about 35 MB RSS (21 MB anonymous) on Linux arm64, which every
optimization run pays.

Add utils/gridinterpolator.py:

- LinearGridInterpolator: 2-D linear interpolation with the same steps
  as SciPy's linear 2-D path (interval search, normalised distances,
  corner weights summed in the same order, fill value outside the grid,
  NaN for NaN input).
- load_grid_interpolator(): reads the shipped pickles with a restricted
  unpickler that maps the SciPy class to a stand-in and accepts only
  the NumPy globals needed for the arrays. SciPy is not imported, and
  the data files no longer depend on SciPy's private module paths.

Results are bit-identical to SciPy on Linux (aarch64, checked on 3.6M
points per table including the query pattern of the inverter). The
macOS arm64 SciPy wheel fuses multiply-add, so there SciPy itself
differs by <= 1.1e-16; GENETIC plans for fixed seeds are unchanged on
both platforms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(prediction): make the SciPy-free table evaluation opt-in

Add optimization.self_consumption_interpolator (scipy | numpy, default scipy).
The default keeps evaluating the self-consumption tables with SciPy's
RegularGridInterpolator, built from the same grid, values and fill value the
pickle holds, so results are unchanged. With numpy the tables are evaluated by
LinearGridInterpolator and SciPy is never imported. The backend is read on
every evaluation, so a config change applies to the next run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(genetic): evaluate candidates in parallel worker processes

A GENETIC run evaluated every candidate in the server process, so a run
used one CPU core however many the machine had. On small ARM boards
(4x Cortex-A55, thermally limited) a 300x200 run took 12-15 minutes.

Candidates are now evaluated in up to two worker processes, forked once
per run after the run is fully prepared. Each worker owns a copy of the
prepared optimizer; only genomes and results cross the process boundary.
The fitness cache stays in the server process: hits are answered there,
and identical genomes in a batch are evaluated once, as a serial run
would hit the cache for the repeats. Results are applied only after the
whole batch returned. If a batch fails or times out, the run continues
serially. For a fixed seed the result is identical to a serial run.

optimization.genetic.workers sets the number of processes. Unset
(default) is automatic: one CPU core always stays free and at most two
are used (1-2 cores -> 1, 3 or more -> 2), capped by a cgroup v2 CPU
limit of the process or any parent (container limit, systemd CPUQuota).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYWR1HVM8a8src23xuyL3T

* feat(ems): notify a URL after every completed optimization

A client that consumes EOS plans (here DVhub) had to poll
/v1/energy-management/optimization/solution to notice a new plan. That
delays the plan by up to the poll interval and makes EOS serialize the
complete solution on every poll, also while it is computing.

ems.notify_url (default None = off) receives an HTTP POST with a small
JSON event ({"event": "optimization_completed", "algorithm",
"completed_at", "duration_s"}) when a run has published its solution.
The POST runs in a daemon thread with a 5 s timeout, so a slow or
unreachable receiver never delays energy management; a failure is
logged once per failure streak. Only http(s) URLs are accepted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYWR1HVM8a8src23xuyL3T

* fix(genetic): freeze the optimization configuration for the duration of a run

The genetic optimization reads horizon_hours, tail_horizon_hours,
interval_sec and prediction.hours from the live configuration on every
access (control_slots, tail_slots, ... are properties). A client that
updates the configuration while a run is in progress - DVhub shrinks or
grows the control horizon as the known price horizon moves during the
night - changed these values mid-run. Genomes and device arrays sized at
the start then no longer matched the simulation loop and the final
evaluation failed:

    IndexError: index 84 is out of bounds for axis 0 with size 84

Long runs on slow hardware hit this almost every night.

optimize_ems() now takes a deep copy of the genetic configuration and the
prediction hours at the start of a run, all reads in genetic.py go through
it, and it is released when the run ends (also on errors). New values apply
to the next run.

Tests: tests/test_genetic_config_frozen_per_run.py reproduces the error
without the fix (horizon 20 -> 24 mid-run: index 20 out of bounds) and
passes with it; genetic/optimization tests 439 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(genetic): pin evaluation workers, keep the server responsive

Workers are pinned to the last `workers` CPU cores (new setting
optimization.genetic.pin_workers, default on); the server process stays
unpinned. The run never spreads beyond `workers` cores without a CPU
quota, and the server and the rest of the system keep the other cores.
No pinning without a spare core.

With a CPU limit (container cpu.max, systemd CPUQuota) the automatic
worker count now keeps a quarter core of it for the server process
(limit 2 -> 1 worker, 2.5 -> 2): two workers used a 2-core limit fully.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYWR1HVM8a8src23xuyL3T

* fix(ems): tolerate settings without notify_url

Test doubles and older configuration objects have no ems.notify_url.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYWR1HVM8a8src23xuyL3T

* fix(devices): evaluate the efficiency curve at the delivered AC load

When the battery limits a discharge (low state of charge or its power
limit), the conversion ran with the efficiency of the requested AC energy.
With a 1000 Wh battery at 10 % SoC, a 1000 W inverter and the curve
[(0, .8), (.5, .95), (1, .95)], a request of 500 Wh AC returned 95 Wh AC
from the available 100 Wh DC, although the curve gives 82.5 % at that load.

The delivered AC energy now solves a = dc * efficiency(a / rated) exactly on
the piecewise-linear curve (solve_ac_for_dc_energy), bounded by the request.
Unlimited discharges and inverters without a curve are unchanged.

Regression tests use a real Battery: low SoC and a power-limited discharge,
each with 60- and 15-minute slots, battery discharge losses, the load path
through process_energy, and constant efficiency.

* docs: regenerate OpenAPI for the efficiency-curve fix

* docs: regenerate OpenAPI after merging the efficiency-curve fix

* docs: regenerate OpenAPI after merging the efficiency-curve fix

* docs: regenerate OpenAPI after merging the efficiency-curve fix

* test(genetic): cover serial fallback on platforms without fork

---------

Co-authored-by: Christin <christin@BigBrother.bikini.bottom.zone>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Andreas <drbacke@gmx.de>
2026-10-11 10:48:04 +02:00
Bobby NoelteandGitHub 894790f577 feat: add pvlib pv forecast provider (#1214)
Add a PV forecast provider that calculates the forecast using a PVLib system model
and weather forecast from the EOS weather forecast provider.

Additional module and inverter models can be easily added as the database
is build from PVLib and SAM databases and a bundled csv file.

The module model and inververt model names are provided by new endpoints
to be used in configuration.

The provider is based on the fantastic work of EMHASS. See
https://github.com/davidusb-geek/emhass/blob/master/src/emhass/forecast.py

A short description of the provider is added to the documentation.

Besides the new features there are the fixes and improvements:

* feat: improve EOSdash config page

* fix: kex_to_series for start_datetime

  Make key_to_series always start the series at start_datetime.

* fix: default provider for GENETIC and GENETIC0 optimization

  To make the default less dependent on internet servers (with API changes and
  availability issues) the default for PVForecast is set to PVForecastPVLib
  and for ElecPrice to ElecPriceFixed. The default weather provider is changed
  to OpenMeteo.

* fix: EOSdash display resampled prediction values

  Make EOSdash display resampled prediction values where  resampling fits to
  the prediction value type. Use bar width that fits to 15 minutes value samples.

* chore: add a UI hints system to EOSdash

  The UI hints system eases the definition of forms for configuration items.
  There are also forms for items in maps and lists. These forms allow to add and delete
  items to/ from  maps and lists. The forms ensure that all required fields of newly
  added items are filled.

* chore: Create an enum for valid optimization algorithms

* chore. Make config also provide the available energy management modes.

  Used for configuration hints.

* chore: Randomize default device id in configuration

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2026-08-07 13:13:17 +02:00
Bobby NoelteandGitHub b67148c47f feat: make home assistant add-on run optimization by default (#940)
When running as Home Assistant add-on the only viable usage is running with
cyclic optimization. Make this the default to als propvide a better experience
for first time users. The optimization will start with demo data, which also
helps to configure Akkudoktor-EOS to the personal usage.

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

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2026-03-13 15:48:43 +01:00
Bobby NoelteandGitHub 237af5289f fix: eosdash startup (#915)
Bump Version / Bump Version Workflow (push) Has been cancelled
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
Adapt uvicorn log level to allowed levels.

Ensure that EOSdash is started after EOS configuration is available.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2026-02-28 11:31:51 +01:00
Christopher NadlerandGitHub 3ccc25d731 Adds inverter AC/DC efficiency and break-even penalty (#888)
Bump Version / Bump Version Workflow (push) Has been cancelled
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
* feat: add inverter AC/DC efficiency and break-even penalty

* test: update tests/test_geneticoptimize.py with new ac_charge_break_even parameter

* docs: update documentation

* chore: update version numbers in configuration files to v0.2.0.dev2602272006923535
2026-02-27 23:12:08 +01:00
Bobby NoelteandGitHub 6498c7dc32 Add database support for measurements and historic prediction data. (#848)
The database supports backend selection, compression, incremental data load,
automatic data saving to storage, automatic vaccum and compaction.

Make SQLite3 and LMDB database backends available.

Update tests for new interface conventions regarding data sequences,
data containers, data providers. This includes the measurements provider and
the prediction providers.

Add database documentation.

The fix includes several bug fixes that are not directly related to the database
implementation but are necessary to keep EOS running properly and to test and
document the changes.

* fix: config eos test setup

  Make the config_eos fixture generate a new instance of the config_eos singleton.
  Use correct env names to setup data folder path.

* fix: startup with no config

  Make cache and measurements complain about missing data path configuration but
  do not bail out.

* fix: soc data preparation and usage for genetic optimization.

  Search for soc measurments 48 hours around the optimization start time.
  Only clamp soc to maximum in battery device simulation.

* fix: dashboard bailout on zero value solution display

  Do not use zero values to calculate the chart values adjustment for display.

* fix: openapi generation script

  Make the script also replace data_folder_path and data_output_path to hide
  real (test) environment pathes.

* feat: add make repeated task function

  make_repeated_task allows to wrap a function to be repeated cyclically.

* chore: removed index based data sequence access

  Index based data sequence access does not make sense as the sequence can be backed
  by the database. The sequence is now purely time series data.

* chore: refactor eos startup to avoid module import startup

  Avoid module import initialisation expecially of the EOS configuration.
  Config mutation, singleton initialization, logging setup, argparse parsing,
  background task definitions depending on config and environment-dependent behavior
  is now done at function startup.

* chore: introduce retention manager

  A single long-running background task that owns the scheduling of all periodic
  server-maintenance jobs (cache cleanup, DB autosave, …)

* chore: canonicalize timezone name for UTC

  Timezone names that are semantically identical to UTC are canonicalized to UTC.

* chore: extend config file migration for default value handling

  Extend the config file migration handling values None or nonexisting values
  that will invoke a default value generation in the new config file. Also
  adapt test to handle this situation.

* chore: extend datetime util test cases

* chore: make version test check for untracked files

  Check for files that are not tracked by git. Version calculation will be
  wrong if these files will not be commited.

* chore: bump pandas to 3.0.0

  Pandas 3.0 now performs inference on the appropriate resolution (a.k.a. unit)
  for the output dtype which may become datetime64[us] (before it was ns). Also
  numeric dtype detection is now more strict which needs a different detection for
  numerics.

* chore: bump pydantic-settings to 2.12.0

  pydantic-settings 2.12.0 under pytest creates a different behaviour. The tests
  were adapted and a workaround was introduced. Also ConfigEOS was adapted
  to allow for fine grain initialization control to be able to switch
  off certain settings such as file settings during test.

* chore: remove sci learn kit from dependencies

  The sci learn kit is not strictly necessary as long as we have scipy.

* chore: add documentation mode guarding for sphinx autosummary

  Sphinx autosummary excecutes functions. Prevent exceptions in case of pure doc
  mode.

* chore: adapt docker-build CI workflow to stricter GitHub handling

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2026-02-22 14:12:42 +01:00
Bobby NoelteandGitHub 7bf9dd723e chore: improve doc generation and test (#762)
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
Improve documentation generation and add tests for documentation.
Extend sphinx by todo directive.

The configuration table is now split into several tables. The test
is adapted accordingly.

There is a new test that checks the docstrings to be compliant to the
RST format as used by sphinx to create the documentation. We can not
use Markdown in docstrings. The docstrings are adapted accordingly.

An additional test checks that the documentation can be build with sphinx.
This test takes very long is only enabled in full run (aka. ci) mode.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2025-11-13 22:53:46 +01:00