Files
EOS/CHANGELOG.md
T
fab3e17446 feat(server): report the process start time in /v1/health (#1386)
* 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>

* perf(genetic): cache the load distribution, clone genomes directly

Two hot spots of a GENETIC run, found by profiling a 15 minute run with EV
and two appliances. The solution is unchanged: both repository reference
inputs give a byte-identical GeneticSolution at 400 generations.

- SelfConsumptionProbabilityInterpolator._load_distribution depends on
  the mean load only, but ran the table interpolation over all minute-load
  bins for every new (load, PV) pair - 27 % of the run. It is now kept per
  (backend, bounded mean load); a run asks for few loads (one per slot,
  plus appliance combinations) with ever-changing PV values. The table is
  static, so entries never go stale; the cache is bounded. Scalar bounds
  use min/max instead of np.clip.
- _make_offspring cloned parents with copy.deepcopy (8 % of the run),
  although the child's fitness, extra_data and immigrant protection are
  dropped right after. A genome is a flat list of ints, so the child is
  created from the parent's genes directly.

Measured (1 process, Apple M5): eHive run with EV + 2 appliances,
200 generations 33.7 s -> 25.1 s; EV only 34.1 s -> 24.3 s.

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

* perf(genetic): cheaper slot simulation, lean lookup cache, batch duplicates

Four hot spots of a GENETIC run, found by profiling a 15 minute run with EV
and two appliances. The solution is unchanged: both repository reference
inputs (optimize_input_1/2, seed 42, 400 generations) give a byte-identical
solution before and after, as do the fixed-seed runs with 1, 2 and 3
evaluation processes.

* Expected direct consumption: the lookup went through the shared
  cache_energy_management store on every slot of every candidate - a key
  built from the callable, a callback per call and room for the last 100
  results only (18 % misses). It is now a dictionary of the interpolator,
  dropped together with that store at the start of an energy management
  run (CacheEnergyManagementStore.generation), bounded to 8192 entries.
* Slot simulation: per-slot inputs are read as plain Python numbers and
  per-slot results are collected in lists, turned into arrays after the
  loop. Element access on NumPy arrays and NumPy scalar arithmetic were the
  largest share of the loop. process_energy() skips the charge/export
  block when a slot has no PV surplus and bounds values without min()/max()
  calls (exact equivalents, NaN and signed zero included).
* Genome keys: bytes() does the 0-255 range check in C, and a genome that
  is relevant as a whole is packed without copying it first.
* Identical genomes within a generation are evaluated once also when the
  fitness cache is off (fitness_cache_max_entries = 0), by grouping the
  batch. It needs no memory beyond the batch; the cache costs 85-95 MB.

Same run, one process, 60 generations (Apple M-series): 7.8 s -> 5.9 s with
appliances, 7.1 s -> 5.0 s without. 200 generations with 3 evaluation
processes: 11.4 s -> 8.2 s.

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

* perf(devices): keep the battery charge bookkeeping in plain numbers

Battery.charge_energy() runs for every slot of every candidate plan. The
per-slot charge limit and charged energy were NumPy arrays read and written
one element at a time, and the NumPy scalars they return made the state of
charge arithmetic slow from then on. They are plain lists now. The lower
charge rates tried when a request does not fit are taken from a tuple built
once per charge_rates array instead of a searchsorted() plus generator per
request.

The results are unchanged (same IEEE doubles, same order of operations).

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

* perf(devices): check the discharge release first, plain discharge bookkeeping

Inverter._discharge_battery_to_ac() looked up the conversion efficiency
before the battery told it that the plan does not release discharge in this
slot. Battery.discharge_released() answers that first; the result of such a
slot was (0.0, 0.0) before and is now returned directly.

The discharged energy per slot is a plain list instead of a NumPy array
read and written one element at a time.

The results are unchanged.

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

* perf(devices): find the efficiency curve segment by bisection

Inverter.dc_to_ac_efficiency_at() runs for every battery discharge of every
candidate plan and walked the curve points one by one. The load fractions
are split off once per curve and the segment is found by bisection; the
segment and the interpolation are those of interpolate_efficiency_curve().
Compared bit for bit against that function for 200000 load values, the
curve points themselves, zero, infinities and NaN.

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

* perf(genetic): sum appliance loads once per candidate, read the SoC directly

The slot loop asked every appliance for its load in every slot (one method
call with a range check each) and converted the state of charge to percent
through a method call twice per slot. The aggregate appliance load is now
summed once per candidate, in the order the loop added the appliances, and
the state of charge is read with the Battery formula in place. Appliances
of another class or with a curve that does not cover the simulated slots,
and batteries with their own current_soc_percentage(), keep the calls.

The results are unchanged.

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

* perf(genetic): compute the control end slot once per run

control_end_slot is a property that reads the interval and the horizon from
the configuration on every access - several hundred thousand times per run.
The configuration is frozen for the duration of a run, so the value is
computed once when the snapshot is taken and dropped with it.

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

* perf(genetic): work out the energy-shift targets once per source slot

_energy_shift_target_slots() runs for every export slot of every
energy-shift mutation, in the server process while the evaluation workers
wait. Which later slots are worth the shift, and in which order, depends on
the forecasts and the source slot only; it is now worked out once per source
slot and run, and the individual only filters the ordered slots. Filtering
the sorted superset gives the same list as sorting the filtered slots (the
sort is stable and its key does not depend on the individual), so mutations
and the random number stream are unchanged.

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

* perf(devices): compute the battery discharge of a slot in one go

Serving load or exporting from the battery went through three nested calls
per slot: Inverter._discharge_battery_to_ac() -> dc_to_ac_efficiency_at()
-> Battery.discharge_energy(), with the release of the slot checked twice
and the efficiency function called also without a curve. For the plain
Battery the inverter now computes the release check, the efficiency and the
discharge in one go; other battery implementations keep the step-by-step
path. The arithmetic is that of Battery.discharge_energy().

The results are unchanged.

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

* perf(genetic): decode appliance starts once per gene combination

Every candidate decoded its appliance start genes (feasibility repair over
cycle windows and minimum gaps) and rebuilt each appliance's load curve over
the whole horizon. The decoded starts, the canonical gene values and the
curves depend on the appliance genes only, and a run sees few different
combinations among tens of thousands of candidates. Each combination is now
decoded and built once per layout (bounded to 4096 combinations) and looked
up afterwards.

The results are unchanged.

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

* perf(genetic): compute only the terminal credit per candidate

Every evaluation called _terminal_value(), which returns the credit for the
energy left in the battery together with a TerminalValueResult report
(component values, diagnostics, curves). The evaluation used the credit and
dropped the report. _terminal_value_credit() computes the same credit
without building the report; the report is still built for the final
solution.

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

* perf(genetic): decode battery states through a lookup table

decode_charge_discharge() decodes every slot on its own from its state, with
about twenty array operations per candidate. The four results per state are
now worked out once per state layout and looked up for the slots of a
candidate. States outside the table keep the explicit decoding.

The results are unchanged.

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

* perf(genetic): check for a full EV before splitting the genome

_repair_ev_charge_at_full_soc() split every candidate's genome and walked
all slots in Python to find EV charging in slots that begin at full SoC.
Nearly no candidate has such a slot. The full slots are now found on the
simulated SoC array first; the genome is split and only those slots are
looked at when there are any.

The results are unchanged.

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

* 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

* perf(genetic): fewer calls per slot in the simulation loop

Three calls ran for every slot of every candidate although most slots do not
need them: the AC charge factor was capped by the inverter also when the
slot has no AC charging, the battery was asked for its discharged energy
through a method, and process_energy() got its export arguments by keyword.
Slots without AC charging skip the cap, the discharged energy is read with
the Battery formula in place, and the arguments are positional. reset() of a
battery creates its two control arrays with zeros() instead of full().

The results are unchanged.

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

* perf(devices): bound the PV surplus and export values without min()/max()

The rest of Inverter.process_energy() - PV surplus, curtailment and battery
export - bounded its values with min()/max() calls. They are conditional
statements now, as the first half of the method already is: 'if x < 0.0:
x = 0.0' is max(x, 0.0) and 'b if b < a else a' is min(a, b), NaN and signed
zero included.

The results are unchanged.

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

* feat(server): report the process start time in /v1/health

A client that pushes forecasts and configuration to EOS has to notice a
restart and push again: a restarted server has lost the imported series
and falls back to its stored configuration. The pid cannot tell - in a
container the server is always pid 1, so a client watching the pid never
saw a container restart and the first runs were cancelled ("Missing or
invalid PV within the control horizon").

/v1/health now carries "started_at", fixed for the life of the process.

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

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

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

---------

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 11:38:17 +02:00

25 KiB

Changelog

All notable changes to the akkudoktoreos project will be documented in this file.

The format is based on Keep a Changelog.

Unreleased

Energy planning update: read before upgrading

This update brings the complete GENETIC energy planner to the current EOS interfaces. It can plan in 15-minute steps, consider battery export, schedule EV charging before departure and fit flexible household loads into their allowed time windows. New measurement tools make missing data, energy use and battery-capacity estimates easier to inspect. The Akkudoktor PV provider also gains an optional local, calibratable model.

Existing installations need an integration check before upgrading. Device settings now use stable IDs instead of list positions. The new POST /v1/optimize takes hardware and schedules from EOS configuration; its request body supplies only current observations, forecasts and a previous plan. The legacy POST /optimize remains available with GENETIC0; changing that URL alone does not migrate an integration to the new planner.

Automatic configuration migration handles supported old settings, but cannot rewrite Home Assistant automations, Node-RED flows or custom scripts. Back up configuration and stored data, then check device IDs, tariff units, fresh battery measurements and the resulting plans. Read the upgrade guide for the compatibility changes and a short test procedure. These changes are not yet a tagged release.

Added

  • Complete GENETIC planning at 15- or 60-minute intervals, timestamp-aligned reuse of previous plans, explicitly enabled battery export, EV deadlines and flexible consumers.
  • Forecast continuation and AUTO/FIXED valuation of energy left in the battery, separate from battery wear costs. Continuation helps choose the plan; it does not add commands beyond the configured control horizon.
  • Optional ems.notify_url: EOS sends an HTTP POST with a small JSON event after every completed optimization, so a client can fetch the new plan right away instead of polling.
  • Configuration-driven POST /v1/optimize and a PDF report of the stored GENETIC result.
  • Measurement sample, energy, household-balance and battery-capacity APIs with data-quality and coverage information. A capacity estimate does not overwrite configured capacity.
  • Optional local, calibratable forecasting within PVForecastAkkudoktor; the remote backend remains the default.
  • Optional load-dependent inverter efficiency for GENETIC: dc_to_ac_efficiency_curve models the lower DC-to-AC efficiency at low load. Without it the constant dc_to_ac_efficiency applies unchanged.
  • Optional size limit for the GENETIC fitness cache: optimization.genetic.fitness_cache_max_entries (null = unbounded, the default; 0 = off; N = at most N keys). Bounding it lowers peak memory of a run by up to ~150 MB on small devices without changing the plan.
  • Optional general.timezone_override: an IANA timezone name used as general.timezone instead of the lookup from latitude/longitude. Unset (default), the lookup is unchanged.
  • GET /v1/health reports started_at, the start time of the server process, so a client notices a restart also in a container (where the pid is always 1).
  • New PV forecast providers giving operators more cloud forecast sources to choose from in addition to Akkudoktor, VRM and Import:
    • PVForecastPVNode — native 15-minute forecasts from the pvnode.com API.
    • PVForecastForecastSolar — forecasts from the free Forecast.Solar API.
    • PVForecastSolcast — forecasts from the Solcast rooftop-site API.
  • Parallel fitness evaluation for GENETIC: optimization.genetic.workers (unset = automatic: at most two worker processes, one CPU core and a container CPU limit stay free; 1 = one process as before) and optimization.genetic.pin_workers (workers pinned to the last cores, default on). For a fixed seed the plan is identical to a single-process run.

Changed / compatibility

  • Device collections are maps keyed by device ID. Old list-based configuration migrates, but integrations using paths such as devices.batteries.0 must use the actual device ID. GENETIC and GENETIC0 have separate algorithm settings.
  • GENETIC rejects missing, stale or invalid battery state and incomplete control forecasts instead of inventing usable inputs. Unsupported device counts and inconsistent battery links also fail explicitly. The default measurement age limit is five minutes.
  • Imported sale prices, including zero and negative prices, are preserved. Corrected battery/inverter limits, losses and wear costs can change the chosen plan and its costs.
  • The electricity-fee framework replaces old elecprice.charges_kwh / vat_rate fields. Those old fields are dropped during migration: configure and verify fees explicitly.
  • Quarter-hour clients must use the returned timestamps and interval, not assume 24 hourly values. Runtime forecast energy is Wh per slot and prices are currency per Wh.
  • Runtime configuration changes now take priority over environment and file values for explicitly updated keys; command-line settings retain higher priority.
  • Lower idle memory: matplotlib (PDF reports), statsmodels (price fallback forecast), pvlib, bs4/lxml, scipy (self-consumption table), the GENETIC0 optimizer and the timezone lookup data are imported on first use instead of at server start. Results are unchanged.
  • New setting optimization.self_consumption_interpolator (scipy by default, numpy). With numpy the self-consumption probability tables are evaluated without SciPy, so a GENETIC run no longer loads SciPy (about 35 MB less memory). Results are bit-identical to SciPy's Linux builds. The tables are read without SciPy in both cases.
  • Lower idle memory: matplotlib (PDF reports), statsmodels (price fallback forecast), pvlib, bs4/lxml, scipy (self-consumption table), the GENETIC0 optimizer and the timezone lookup data are imported on first use instead of at server start. Results are unchanged.
  • Faster GENETIC runs with unchanged plans (about 60 % less run time on a Raspberry Pi 4): table lookups, the appliance starts and the battery state decoding are worked out once per run instead of once per candidate, the slot simulation and the battery bookkeeping work on plain numbers instead of single NumPy elements, the terminal value report is built for the final solution only, offspring are created without deepcopy, and identical genomes of a generation are evaluated once also when the fitness cache is off.

Fixed

  • Failed optimization requests no longer return a previous successful result as if it belonged to the failed run; successful results and plans are published together.
  • Restored JSON measurements retain their timestamps and values.
  • Configuration updates made at runtime, e.g. by PUT /v1/config, are no longer discarded when the same configuration key is set in the EOS configuration file or in the environment (#1303).
  • Environment variables are applied to the configuration on server startup instead of taking effect only after the first configuration change.
  • GENETIC reads its horizon, interval and prediction settings once at the start of a run. A configuration change during a run (for example a client adapting the control horizon) no longer fails the run with an out-of-bounds error; it applies to the next run.

0.3.0 (2026-03-17)

Akkudoktor-EOS can now be run as Home Assistant add-on and standalone. As Home Assistant add-on EOS uses ingress to fully integrate the EOSdash dashboard in Home Assistant.

Adapters for Home Assistant and NodeRed integration are added. These adapters provide a simplified interface to these HEMS besides the standard REST interface.

The prediction and measurement data can now be backed by a database. The database allows to keep historic prediction data and measurement data for long time without keeping it in memory. The database supports backend selection, compression, incremental data load, automatic data saving to storage, automatic vacuum and compaction. Two database backends are integrated and can be configured, LMDB and SQLight3.

New prediction providers allow to access OpenMeteo weather data and to define fixed electricity prices for configurable time windows.

An anoying bug in the genetic algorithm that created unfeasable battery charge and discharge amounts is now hopefully fixed.

In addition, bugs were fixed and new features were added.

Feat

  • add inverter AC/DC efficiency and break-even penalty
  • add database support for measurements and historic prediction data. The prediction and measurement data can now be backed by a database. The database allows to keep historic prediction data and measurement data for long time without keeping it in memory. Two database backends are integrated and can be configured, LMDB and SQLight3.
  • add adapters for integrations Adapters for Home Assistant and NodeRED integration are added. Akkudoktor-EOS can now be run as Home Assistant add-on and standalone. As Home Assistant add-on EOS uses ingress to fully integrate the EOSdash dashboard in Home Assistant.
  • add make repeated task function make_repeated_task allows to wrap a function to be repeated cyclically.
  • allow eos to be started with root permissions and drop priviledges Home assistant starts all add-ons with root permissions. Eos now drops root permissions if an applicable user is defined by paramter --run_as_user. The docker image defines the user eos to be used.
  • make home assistant add-on run optimization by default 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.
  • make eos supervise and monitor EOSdash Eos now not only starts EOSdash but also monitors EOSdash during runtime and restarts EOSdash on fault. EOSdash logging is captured by EOS and forwarded to the EOS log to provide better visibility.
  • add openmeteo weather provider
  • add fixed electricity prediction with time window support
  • add duration to string conversion Make to_duration to also return the duration as string on request.

Fixed

  • genetic optimizer charge rates and soc accuracy
  • energy charts bidding zone in request
  • prevent exception when load prediction data is missing
  • eosdash startup Ensure that EOSdash is only started after EOS configuration is available.
  • 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.
  • startup with no config Make cache and measurements complain about missing data path configuration but do not bail out.
  • 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.
  • dashboard bailout on zero value solution display Do not use zero values to calculate the chart values adjustment for display.
  • openapi generation script Make the script also replace data_folder_path and data_output_path to hide real (test) environment pathes.
  • development version scheme The development versioning scheme is adaptet to fit to docker and home assistant expectations. The new scheme is x.y.z and x.y.z.dev'date''hash'. Hash is only digits as expected by home assistant. Development version is appended by .dev as expected by docker.
  • use mean value in interval on resampling for array When downsampling data use the mean value of all values within the new sampling interval.
  • default battery ev soc and appliance wh Make the genetic simulation return default values for the battery SoC, electric vehicle SoC and appliance load if these assets are not used.
  • import json string Strip outer quotes from JSON strings on import to be compliant to json.loads() expectation.
  • default interval definition for import data Default interval must be defined in lowercase human definition to be accepted by pendulum.
  • clearoutside schema change

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.
  • 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.
  • introduce retention manager A single long-running background task that owns the scheduling of all periodic server-maintenance jobs (cache cleanup, DB autosave, …)
  • guard against visualization errors in genetic optimization
  • improve provider update error handling and add VRM provider settings validation
  • canonicalize timezone name for UTC Timezone names that are semantically identical to UTC are canonicalized to UTC.
  • extend config file migration for default value handling
  • extend datetime util test cases
  • 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.
  • 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.
  • 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.
  • remove sci learn kit from dependencies The sci learn kit is not strictly necessary as long as we have scipy.
  • add documentation mode guarding for sphinx autosummary Sphinx autosummary excecutes functions. Prevent exceptions in case of pure doc mode.
  • adapt docker-build CI workflow to stricter GitHub handling
  • add CodeQL analysis workflow to CI
  • Use info logging to report missing optimization parameters In parameter preparation for automatic optimization an error was logged for missing paramters. Log is now down using the info level.
  • make EOSdash use the EOS data directory for file import/ export EOSdash use the EOS data directory for file import/ export by default. This allows to use the configuration import/ export function also within docker images.
  • improve EOSdash config tab display Improve display of JSON code and add more forms for config value update.
  • make docker image file system layout similar to home assistant Only use /data directory for persistent data. This is handled as a docker volume. The /data volume is mapped to ~/.local/share/net.akkudoktor.eos if using docker compose.
  • add home assistant add-on development environment Add VSCode devcontainer and task definition for home assistant add-on development.
  • Use uv to manage the virtual environment for development. This enormously increases dependency updates.
  • improve documentation

0.2.0 (2025-11-09)

The most important new feature is automatic optimization. EOS can now independently perform optimization at regular intervals. This is based on the configured system parameters and forecasts, and also uses supplied measurement data, such as the current battery SoC. The result is an energy-management plan as well as the optimization output. The existing optimization interface using POST /optimize remains available and can still be used as before.

In addition, bugs were fixed and new features were added:

  • Automatic optimization creates a default configuration if none is provided. This is intended to make it easier to create a custom configuration by adapting the default.
  • The parameters of the genetic optimization algorithm (number of generations, etc.) are now configurable.
  • For home appliances, start windows can now be specified (experimental).
  • Configuration files from previous versions are converted to the current format on first launch.
  • There are now measurement keys that are permanently assigned to a specific device simulation. This simplifies providing measurement values for device simulations (e.g. battery SoC).
  • The infrastructure and first applications for feed-in tariff forecasting (currently only fixed tariffs) are now integrated.
  • EOSdash has been expanded with new tabs for displaying the energy-management plan and predictions.
  • The documentation has been updated and expanded in many places.

Feat

  • Energy-management plan generation based on S2 standard instructions

  • Feed-in-tariff prediction support (incl. tests & docs)

  • LoadAkkudoktorAdjusted load prediction variant

  • Standardized measurement keys for battery/EV SoC

  • Measurement keys configurable via EOS configuration

  • Setup default device configuration for automatic optimization

  • Health endpoints show version + last optimization timestamps

  • Configuration of genetic algorithm parameters

  • Configuration options for home-appliance time windows

  • Mitigation of legacy configuration

  • Config backup enhancements:

    • Timestamp-based backup IDs
    • API to list backups
    • API to revert to a specific backup
    • EOSdash Admin tab integration
  • Pendulum date types via pydantic_extra_types.pendulum_dt

  • Time, TimeWindow, TimeWindowSequence, and to_time helpers in datetimeutil

  • Extended DataRecord with configurable field-like semantics

  • EOSdash: Solution view now displays genetic optimization results and aggregated totals

  • EOSdash UI:

    • Plan tab
    • Predictions tab
    • Cache management in Admin tab
    • About tab
  • Pydantic merge model tests

  • Developer profiling entry in Makefile

  • Changelog & docs updated for commitizen release flow

  • Developer documentation updated

  • Improved install & development documentation

Changed

  • Battery simulation

    • Performance improvements
    • Charge + start times now reflect realistic simulation
  • Appliance simulation:

    • Time windows may roll over to next day
  • Revised load prediction by splitting original LoadAkkudoktor into:

    • LoadAkkudoktor
    • LoadAkkudoktorAdjusted

Fixed

  • Correct URL/path for Akkudoktor forum in README

  • Automatic optimization:

    • Reuses previous start solution
    • Interval execution + locking + new endpoints
    • Properly loads required data
    • EV charge-rate migration for proper availability
  • Genetic common settings consistently available

  • Config markdown generation

  • Recognize environment variables on EOS server startup

  • Remove 0.0.0.0 → localhost translation on Windows

  • Allow hostnames as well as IPs

  • Access Pydantic model fields via class instead of instance

  • Down-sampling in key_to_array

  • /v1/admin/cache/clear clears all cache files; added /clear-expired

  • Use tzfpy instead of timezonefinder for more accurate EU timezones

  • Explicit provider settings in config instead of union

  • ClearOutside weather prediction irradiance calculation

  • Test config file priority without config_eos fixture

  • Complete optimization sample-request documentation

  • Replace gitlint with commitizen

  • Synchronize pre-commit config with real dependencies

  • Add missing babel to requirements

  • Fix documentation, tests, and implementation around optimization + predictions

Chore

  • Use memory cache for inverter interpolation
  • Refactor genetic modules (split config, remove device singleton)
  • Rename memory cache to CacheEnergyManagementStore
  • Use class properties for config/EMS/prediction mixins
  • Skip matplotlib debug logs
  • Auto-sync Bokeh JS CDN version
  • Rename hello.py → about.py in EOSdash
  • Remove EOSdash demo page
  • Split server test from system test
  • Move doc utils to generate_config_md.py
  • Improve documentation for pydantic merge models
  • Remove pendulum warning from README
  • Drop GitHub Discussions from contributing docs
  • Rename or reorganize files / classes during refactors

BREAKING CHANGES

EOS configuration + v1 API have changed:

  • available_charge_rates_percent removed → replaced by charge_rate
  • Optimization parameter hours → renamed to horizon_hours
  • Device config must explicitly list devices + properties
  • Prediction providers now explicit (instead of union)
  • Measurement keys provided as lists
  • Feed-in-tariff providers must be explicitly configured
  • /v1/measurement/loadxxx endpoints removed → use generic measurement endpoints
  • /v1/admin/cache/clear now clears *all- cache files; /v1/admin/cache/clear-expired only clears expired entries

v0.1.0 (2025-09-30)

Feat

  • added Changelog for 0.0.0 and 0.1.0

v0.0.0 (2025-09-30)

This version represents one year of development of EOS (Energy Optimization System). From this point forward, release management will be introduced.

Feat

Core Features

  • energy Management System (EMS) with battery optimization
  • PV (Photovoltaic) forecast integration with multiple providers
  • load prediction and forecasting capabilities
  • electricity price integration
  • VRM API integration for load and PV forecasting
  • battery State of Charge (SoC) prediction and optimization
  • inverter class with AC/DC charging logic
  • electric vehicle (EV) charging optimization with configurable currents
  • home appliance scheduling optimization
  • horizon validation for shading calculations

API & Server

  • migration from Flask to FastAPI
  • RESTful API with comprehensive endpoints
  • EOSdash web interface for configuration and visualization
  • Docker support with multi-architecture builds
  • web-based visualization with interactive charts
  • OpenAPI/Swagger documentation
  • configurable server settings (port, host)

Configuration & Data Management

  • JSON-based configuration system with nested support
  • configuration validation with Pydantic
  • device registry for managing multiple devices
  • persistent caching for predictions and prices
  • manual prediction updates
  • timezone support with automatic detection
  • configurable VAT rates for electricity prices

Optimization

  • DEAP-based genetic algorithm optimization
  • multi-objective optimization (cost, battery usage, self-consumption)
  • 48-hour prediction and optimization window
  • AC/DC charging decision optimization
  • discharge hour optimization
  • start solution enforcement
  • fitness visualization with violin plots
  • self-consumption probability interpolator

Testing & Quality

  • comprehensive test suite with pytest
  • unit tests for major components (EMS, battery, inverter, load, optimization)
  • integration tests for server endpoints
  • pre-commit hooks for code quality
  • type checking with mypy
  • code formatting with ruff and isort
  • markdown linting

Documentation

  • conceptual documentation
  • API documentation with Sphinx
  • ReadTheDocs integration
  • Docker setup instructions
  • contributing guidelines
  • English README translation

Providers & Integrations

  • PVForecast.Akkudoktor provider
  • BrightSky weather provider
  • ClearOutside weather provider
  • electricity price provider

Refactor

  • optimized Inverter class for improved SCR calculation performance
  • improved caching mechanisms for better performance
  • enhanced visualization with proper timestamp handling
  • updated dependency management with automatic Dependabot updates
  • restructured code into logical submodules
  • package directory structure reorganization
  • improved error handling and logging
  • Windows compatibility improvements

Fix

  • cross-site scripting (XSS) vulnerabilities
  • ReDoS vulnerability in duration parsing
  • timezone and daylight saving time handling
  • BrightSky provider with None humidity data
  • negative values in load mean adjusted calculations
  • SoC calculation bugs
  • AC charge efficiency in price calculations
  • optimization timing bugs
  • Docker BuildKit compatibility
  • float value handling in user horizon configuration
  • circular runtime import issues
  • load simulation data return issues
  • multiple optimization-related bugs

Build

  • Python version requirement updated to 3.10+
  • added Bandit security checks
  • improved credential management with environment variables

Dependencies

Major dependencies included in this release:

  • FastAPI 0.115.14
  • Pydantic 2.11.9
  • NumPy 2.3.3
  • Pandas 2.3.2
  • Scikit-learn 1.7.2
  • Uvicorn 0.36.0
  • Bokeh 3.8.0
  • Matplotlib 3.10.6
  • PVLib 0.13.1
  • Python-FastHTML 0.12.29

Notes

Development Notes

This version encompasses all development from the initial commit (February 16, 2024) through September 29, 2025. The project evolved from a basic energy optimization concept to a comprehensive energy management system with:

  • 698+ commits
  • multiple contributor involvement
  • continuous integration/deployment setup
  • automated dependency updates
  • comprehensive testing infrastructure

Migration Notes

As this is the initial versioned release, no migration is required. Future releases will include migration guides as needed.