Commit Graph
119 Commits
Author SHA1 Message Date
Bobby NoelteandClaude Opus 5.5 a0e57a18b7 fix(config): validate prediction providers before the prediction exists
The provider settings (weather, PV forecast, load, electricity price, feed-in
tariff, electricity fee) validate their provider id against the providers of
the prediction instance. While the configuration is created, that instance
does not exist yet, and the validation falls back to fixed lists. These were
incomplete: weather knew only 'WeatherImport', load lacked
'LoadAkkudoktorAdjusted'. A configuration file with e.g. weather provider
'OpenMeteo' then fails to load:

  Provider 'OpenMeteo' is not a valid weather provider: ['WeatherImport']

This is hidden today because ConfigEOS.documentation_mode() is always true
(_force_documentation_mode is a pydantic private attribute, whose class
attribute is a truthy descriptor), and get_prediction() then creates the
prediction instance on access. Once documentation mode is enabled only
explicitly, the configuration does not load.

Derive the provider ids from the provider classes instead
(prediction_provider_ids). They no longer depend on whether the prediction
instance exists, so the validation is the same before and after
initialization.

Additional changes
- Add the test fixture uncreated_config, which lets get_config(init=True)
  create a new configuration from the config file and restores the old one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 12:40:15 +01:00
628226d3d9 feat(pvforecast): model snow on the modules in the local backend (#1484)
No PV forecast modelled snow lying on the modules, so after a snowfall the
optimizer planned with PV that never came. The local Open-Meteo/pvlib
backend can now reduce the forecast by the snow coverage (opt-in via
pvforecast.akkudoktor.snow_enabled):

- Request hourly snowfall and snow depth with the weather and run the NREL
  snow coverage model (pvlib.snow) per plane.
- Correct the coverage with measured PV production: production above the
  model clears it at once; low production raises it only while snow is
  plausible, so curtailment and inverter outages are not learned as snow.
  Persisting snow damps the slide rate, fading with
  snow_feedback_memory_hours.
- Rebuild the coverage from snow_lookback_days on every update instead of
  storing state.
- Exclude intervals with possible snow from the calibration fit.
- Warn when PV meter readings are missing or stale; ignore impossible meter
  jumps.

_plane_power keeps its two-value signature (used by the adaptive provider in
#1422); the irradiance comes from the new _plane_power_and_poa.

Tests replay real February 2026 data from one plant (snow on 02-19/20).

Signed-off-by: SchaafAlexander <86607129+SchaafAlexander@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 07:42:20 +01:00
NormannandGitHub b33953d445 fix(pvforecast): normalize Forecast.Solar power before resampling (#1487)
Forecast.Solar supplies instantaneous power with irregular sunrise/sunset timestamps. GENETIC interprets stored values as interval averages and infers cadence from the shortest gap, so these samples create artificial coverage gaps. The prediction list API also averages away the sunset zero before interpolating, producing positive nighttime PV.

Integrate each roof plane's piecewise-linear power curve into UTC-contiguous 15-minute interval-average W before summing planes. Both hourly and quarter-hour GENETIC slots then consume the same normalized data as the prediction API, with exactly one downstream W-to-Wh conversion. Explicit sunrise/sunset zero boundaries establish the adjacent night within each forecast day. Daylight gaps longer than an hour, invalid power, absent planes and unavailable forecast days remain missing; the strict GENETIC coverage check stays in place.

Refreshes replace the current forecast and remove stale tails. Because cadence detection also sees historical records, the first refresh clears legacy forecasts containing non-aligned timestamps, including their history. Later refreshes retain normalized history. This upgrade behavior is documented.

Forecast validation errors now identify the first invalid interval and the number of invalid slots.
2026-11-07 19:46:58 +01:00
f804d48072 feat(pvforecast): add adaptive per-plane PV learning and diagnostics (#1422)
PV arrays with different orientations and curtailment need separate plant calibration and weather correction. This opt-in provider publishes normal EOS AC/DC forecast keys at 15-minute resolution.

- Preserve daily per-plane energy/shape calibration and validated short-term local/trend selection. Curtailment-sensitive arrays require a fresh release flag or an explicitly configured learning guard.
-  Add switchable empirical solar-angle response relative to separate eligible reference arrays, removing their common weather error. This learns angular residuals, not physical angles or a measured geometric horizon.
-  Archive actually received weather model inputs, first local receipt and frozen per-member PV candidates. Returned past weather is excluded; unknown source issuance remains explicit. Learn model weights and forecast-regime bias separately at 2–6, 6–24 and 24–48 hours.
- New long-horizon candidates remain in shadow until their genuinely issued outputs improve equal-day MAE by the configured margin, win on at least 60% of validation days and pass a matched-support energy-bias guard. Insufficient evidence retains calibration. Daily yields still require complete local days, including DST.
- Expose fitted parameters, independent training/validation day counts, errors, selected methods and persistence in the existing status API, with additional normal prediction keys for Grafana. Retain physical DC through angular/weather corrections an  enforce individual inverter AC limits.

Co-authored-by: Normann <github@koldrack.com>
Co-authored-by: Christin <chrissi@von-perbandt.de>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: YMT <yn276364@dal.ca>
Co-authored-by: sarankumar1325 <120080287+sarankumar1325@users.noreply.github.com>
2026-11-03 15:31:32 +01:00
NormannandGitHub 8396bf24ae fix(deps): upgrade pandas to 3.1 and fix EOS deprecations (#1469)
* build(deps): bump pandas from 3.0.6 to 3.1.0

* fix(prediction): avoid pandas 3.1 deprecation warnings
2026-10-29 09:10:21 +01:00
58b968562e fix(adapter): stamp Home Assistant device states with run time, not slot start (#1452)
* fix(adapter): stamp Home Assistant device states with run time, not slot start

The Home Assistant adapter stored every reading with ems_start_datetime,
which set_start_datetime() rounds down to the optimization slot (e.g.
14:00 for a run at 14:11). The GENETIC configuration request checks SoC
freshness against the unrounded observation_datetime with
measurement_max_age_seconds (default 300 s). Any run starting more than
five minutes into a slot therefore discarded the freshly read SoC and
optimized with 0 % initial SoC.

Stamp device state measurements (device_measurement_entity_ids, e.g.
SoC) with observation_datetime instead. Not to_datetime(): a reading
taken a few seconds after the observation time would be rejected as a
future sample. Meter readings (load, grid import/export, PV) keep the
slot start, because their energy is aggregated per slot.

With device states at their real time, a record holding only some keys
can now be the newest one (e.g. the Home Assistant battery SoC while the
EV SoC or a cycle count comes through the measurement API). Readers
therefore skip records without the value they need:

- GENETIC SoC and completed appliance cycles: such a record no longer
  hides a fresh value (the cycle count case aborted the optimization).
  This also covers a meter reading at the slot start that is newer than
  a SoC supplied through the measurement API. Values stored as pandas NA
  count as missing instead of raising TypeError; an explicit NaN stays
  an invalid sample.
- GENETIC0 SoC: looks up the reading nearest to observation_datetime
  instead of the slot start, skipping records without SoC and converting
  only the selected sample. Before, a run late in the slot (14:40 in the
  14:00 slot) would pick the previous run's reading (13:40). This also
  fixes SoC values PUT via the REST API with their real time.

LoadAkkudoktorAdjusted anchored its hourly comparison window on the
newest record of any key. An off-hour device state would shift every
bucket (e.g. to :11) against hour-aligned meter readings; the window now
starts and ends on full hours, and less than one full hour of data
yields no adjustment instead of an error. (15-minute slot starts and SoC values
PUT with their real time had the same effect before.)

Regenerated openapi.json and docs/_generated/openapi.md (version only).

Fixes #1447

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

* test(genetic0): use pv_battery instead of deprecated pv_akku

mypy flags attribute access on the pv_akku computed field
(attr-defined), which failed the pre-commit check of the PR.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 07:14:00 +01:00
sarankumarandGitHub cdbaae45ba fix(elecprice): handle mixed UTC offsets in Tibber prices around DST (#1448)
Once Tibber publishes prices across a DST change, the series holds
timestamps with two UTC offsets and pd.to_datetime() raises "Mixed
timezones detected", so every optimization run is canceled. Parse via
UTC and convert back to the configured timezone.

Fixes #1446
2026-10-25 06:10:48 +01:00
7dc1ee92fa perf(genetic): faster runs with unchanged results (#1385)
* 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

* 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

---------

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:37:43 +02:00
560370281f perf(prediction): optional SciPy-free evaluation of the self-consumption tables (#1378)
* 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>

---------

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 09:38:14 +02:00
3651d33915 perf: import heavy optional dependencies on first use (#1377)
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: Christin <christin@BigBrother.bikini.bottom.zone>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:40:43 +02:00
NormannandGitHub b7610e8274 fix: detect recent Energy-Charts cadence for feed-in coverage (#1362)
* fix: use recent original feed-in cadence for publication coverage

* test: cover mixed Energy-Charts feed-in source cadence
2026-10-11 08:39:20 +02:00
NormannandGitHub e203a81666 fix: surface DVhub feed-in tariff update failures (#1357)
* fix: surface DVhub feed-in tariff update failures

* test: annotate DVhub sample records for mypy

* fix: require real feed-in tariff coverage in GENETIC0

* test: satisfy GENETIC0 tariff coverage checks

* test: patch DVhub storage method on its class
2026-10-11 08:39:01 +02:00
04f28997ea feat: deliver complete GENETIC optimization to main (#1330)
* feat: adapt configuration for multi optimization algorithms

Decouple configuration from optimization algorithm parameters. Add to_[algorithm]_param() methods
to the configuration that derive optimization algorithm specific parameters from the configuration.
Add x-scope tags to the configuration options that describe for which specific algorithms the
configuration option is for.

The whole device settings are restructured. There are now general settings for the device classes
with the afore mentioned to_[algorithm]_param() methods. The general device settings got their own
directory `devices/settings`. By this the parameter class also does not have to be a pydantic model
which can be used for future optimization/ simulations speed up.

Also the parameter class for a device is now part of the device module. This better decouples and
also is the natural place for parameters of a device.

Besides this feature there are also fixes and improvements:

* feat: extend home appliance time window settings and simulation

  Home appliance can now be configured for multiple runs with per-cycle allowed time windows. The
  number of remaining cycles to plan is determined at runtime by reading the
  ``cycles_completed_measurement_key`` from the measurement store.

* feat: specialiced CycleTimeWindowSequence for time window sequences

  Sequence of time windows associated to cycles.

  This model specializes ``ValueTimeWindowSequence`` so that the ``value``
  field of each ``ValueTimeWindow`` encodes the **cycle index** (0-based
  integer) the window belongs to.

  Typical use: an appliance that must run ``n`` times per day, each run
  constrained to a distinct time window.  Assign ``value=0`` to windows
  for the first cycle, ``value=1`` for the second, and so on.  Multiple
  windows may share the same cycle index (their allowed regions are unioned).
  Windows with ``value=None`` are silently ignored by all cycle-aware methods.

* fix: Make test_configmigrate also regard the _ANY_SENTENIEL in key values

* chore: Make devices configurations a map instead of a list

  This makes config paths stable regardless of declaration order and lets each device settings
  class build its own config path from ``self.device_id`` without needing an external index.
  Tests are adapted likewise.

  Devices configurations are automatically migrated from lists to maps.

* chore: rename levelized_cost_of_storage_kwh to levelized_cost_of_storage_amt kwh

  This better fits in the naming scheme and also makes clear the costs are money.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>

* fix: runtime config update ignored by config file

Runtime settings were handed back to pydantic-settings as init settings,
which rank below the config file and the environment. Any key already
present in EOS.config.json or in the environment silently discarded the
update, so a bulk PUT /v1/config returned 200 without applying anything,
while the granular PUT /v1/config/{path} endpoint kept working.

Add a dedicated runtime settings source ranked directly below the command
line arguments and record granular updates there as well, so both
endpoints share one store that survives re-evaluation of the settings
sources. Environment variables keep precedence over the config file for
all keys that were not set at runtime.

Also repairs revert_settings() and update(), which passed their data
through the same init settings.

Closes #1303

* fix: env vars ignored on first config build

ConfigEOS.__init__ passed self as first positional argument to _setup,
which forwards it to pydantic_settings.BaseSettings.__init__. Its first
positional parameter is _case_sensitive, so the environment source
matched the upper case variable names against the lower case field names
and returned nothing. Environment settings only took effect after the
next configuration setup.

* docs: changelog for config priority fixes

* fix(config): preserve device identities and storage costs during migration

* fix(measurement): restore JSON records into the existing singleton

* fix(devices): preserve charge-rate typing and public import compatibility

* feat(measurement): integrate typed energy quality and capacity APIs

Port the locally backed-up measurement extensions to main async storage and PR #1256 device maps. Preserve runtime capacity estimates across #1305 bulk updates. Confirm JSON singleton restore defect on unchanged main and add regression. No production configuration or measurements included.

Co-authored-by: Andreas <drbacke@gmx.de>

* docs(measurement): describe household settings and consolidate regression coverage

* docs(measurement): regenerate configuration and API contracts

* test(measurement): isolate capacity database state between tests

* ruff format fix

* fix(measurement): restore JSON records into the existing singleton

* test(measurement): assert restored timestamps before timezone conversion

* test(measurement): assert restored timestamps before timezone conversion

* fix: preserve imported feed-in revenue during parameter preparation

Cancel GENETIC preparation when imported revenue cannot be read or contains invalid values, preserving the chosen provider instead of replacing it with demo tariffs. Keep valid positive, zero and negative amount/Wh series unchanged.

Adapt the revenue-preservation regressions from PRs #1224 and #1304 to the async main API, including real timestamped imports and simulation repricing. The feature-only direct-marketing override remains outside this main fix.

Co-authored-by: Christin <info@bikinibottom.capital>

Co-authored-by: Normann <github@koldrack.com>

* feat(devices): port slot-aware battery export and direct-use physics

Port scoped device changes from d2e2d58237. Keep PR #1256 parameter conversion structure and separate GENETIC0 devices. Validate physical flows and reprice changed simulation results.

Co-authored-by: Andreas <drbacke@gmx.de>

Co-authored-by: Christin <info@bikinibottom.capital>

* docs(measurement): align API version with refreshed prerequisites

* fix: return only completed optimization results per run

* feat(pvforecast): add calibrated local Akkudoktor backend

Port local PV modeling and outage calibration from feature commits f976335, 6dc58c3 and faed0fd by Andreas. Keep PVForecastAkkudoktor identity and remote default, adapt to async storage, and migrate legacy provider settings.

* fix(cache): distinguish callables in the shared EMS cache

Include the function object in cache keys so methods of one interpolator cannot reuse a probability as a power value. Cover both call orders, keyword arguments, cache hits and separate closures with identical qualified names.

* fix(devices): constrain the physics port and validate export levels

Defer inactive EV deadline fields to the optimizer port, reject nonfinite export rates, and document the hourly Optimize boundary. Verify converter IDs, rates and LCOS, separate GENETIC0 interpolation, physical boundary flows and independent GENETIC repricing.

* docs(pvforecast): regenerate local backend configuration schema

* docs(devices): regenerate slot-physics configuration and OpenAPI schemas

* test: type dynamic Optimize regression arguments

* style: wrap imported tariff test parameter import

* style(pvforecast): apply CI import formatting

* docs(pvforecast): refresh API version after CI formatting

* fix(config): satisfy typed device conversion and migration contracts

* docs(config): refresh validated configuration prerequisite schemas

* fix(measurement): enforce typed capacity and sample validation

* test(devices): align physics regressions with strict type checking

* style(measurement): normalize imports for CI

* docs(measurement): refresh typed measurement API schemas

* docs(devices): refresh API version after prerequisite merge

* test: make optimization dispatch timezones explicit

* docs(interpolator): use portable reStructuredText markup

* docs(devices): refresh API version after docstring compatibility fix

* feat: complete configuration-driven GENETIC optimization and reports (#1329)

* feat(devices): port slot-aware battery export and direct-use physics

Port scoped device changes from d2e2d58237. Keep PR #1256 parameter conversion structure and separate GENETIC0 devices. Validate physical flows and reprice changed simulation results.

Co-authored-by: Andreas <drbacke@gmx.de>

Co-authored-by: Christin <info@bikinibottom.capital>

* feat(optimization): port tested terminal and tail value primitives

Source d2e2d58237. 22 primitive tests pass; integration with the optimizer, forecast horizon and API is still pending.

Co-authored-by: Andreas <drbacke@gmx.de>

Co-authored-by: Christin <info@bikinibottom.capital>

* fix(devices): preserve charge-rate typing and public import compatibility

* feat(measurement): integrate typed energy quality and capacity APIs

Port the locally backed-up measurement extensions to main async storage and PR #1256 device maps. Preserve runtime capacity estimates across #1305 bulk updates. Confirm JSON singleton restore defect on unchanged main and add regression. No production configuration or measurements included.

Co-authored-by: Andreas <drbacke@gmx.de>

* test(integration): validate optimizer economics and document measurement settings

* docs(integration): record tested checkpoint and remaining consolidation work

* docs(development): define isolated PR packages and remaining porting gates

* docs(integration): refresh API version after measurement reconciliation

* docs(integration): record PR readiness verification results

* docs(development): record publication and verification of PR 1322

* test(measurement): assert restored timestamps before timezone conversion

* docs(development): record corrected PR head and CI progress

* docs(integration): refresh API version after prerequisite alignment

* docs(integration): define parallel packages and Optimize compatibility gates

* fix: preserve imported feed-in revenue during parameter preparation

Cancel GENETIC preparation when imported revenue cannot be read or contains invalid values, preserving the chosen provider instead of replacing it with demo tariffs. Keep valid positive, zero and negative amount/Wh series unchanged.

Adapt the revenue-preservation regressions from PRs #1224 and #1304 to the async main API, including real timestamped imports and simulation repricing. The feature-only direct-marketing override remains outside this main fix.

Co-authored-by: Christin <info@bikinibottom.capital>

Co-authored-by: Normann <github@koldrack.com>

* test(integration): verify tariff protection with mapped device physics

* fix: return only completed optimization results per run

* test(integration): verify algorithm aliases and mapped-device contracts

* fix(cache): distinguish callables in the shared EMS cache

Include the function object in cache keys so methods of one interpolator cannot reuse a probability as a power value. Cover both call orders, keyword arguments, cache hits and separate closures with identical qualified names.

* fix(devices): constrain the physics port and validate export levels

Defer inactive EV deadline fields to the optimizer port, reject nonfinite export rates, and document the hourly Optimize boundary. Verify converter IDs, rates and LCOS, separate GENETIC0 interpolation, physical boundary flows and independent GENETIC repricing.

* feat(pvforecast): add calibrated local Akkudoktor backend

Port local PV modeling and outage calibration from feature commits f976335, 6dc58c3 and faed0fd by Andreas. Keep PVForecastAkkudoktor identity and remote default, adapt to async storage, and migrate legacy provider settings.

* docs(integration): record combined compatibility checks and green JSON PR CI

* test: type dynamic Optimize regression arguments

* docs(integration): record Optimize fix PR publication

* docs(integration): record imported tariff protection PR

* style(pvforecast): apply CI import formatting

* style(integration): align combined regression imports

* test: make optimization dispatch timezones explicit

* docs(interpolator): use portable reStructuredText markup

* chore: validate combined integration with locked mypy

* docs: hand off six validated pull requests for manual review

* feat: report genetic interval and terminal value diagnostics

* feat(devices): reconcile flexible profiles and EV deadlines with cycle scheduling

Adapt the flexible consumer primitives from d2e2d582 while retaining the keyed settings and per-cycle scheduling introduced by #1256. Preserve slot battery physics and GENETIC0 flat-load conversion. Cover energy conservation, deadlines, window intersections, DST, completed cycles and EV converters.

* test: satisfy typed genetic PDF chart contracts

* feat(optimization): resolve quarter-hour GENETIC requests from configuration

* test(genetic): verify real device scheduling, measurement and export contracts

Register appliance completed-cycle measurement keys so the real store accepts both default and custom counters. Exercise complete low-budget optimizer runs, persisted measurements, generic solution output and instructions, including zero-power phases, EV departure boundaries, per-cycle windows and LCOS.

* fix: bound genetic report forecasts to executable horizon

* feat: complete native genetic scheduling and retained result contracts

* fix: retain missing raw samples when dropna is disabled

* fix: align local optimization slots and measurement instants

* docs: explain complete genetic rollout and PR dependencies

* feat: expose retained GENETIC report through the versioned API

* docs: regenerate complete genetic configuration and API schema

* docs: format consolidation and review handoff markdown

* test: align isolated EMS fixture with native genetic run options

* test(genetic): clean up singleton measurements after device integration tests

* test: freeze the clock without replacing timestamp conversion

* fix: preserve explicit warmstart timezones in runtime requests

* test(genetic): validate device schedules in UTC and Berlin

Use explicit Berlin origins for Berlin wall-clock windows, compare absolute deadline instants correctly, and run all real device optimizer scenarios under both UTC and Europe/Berlin. Compare exported starts in the run timezone instead of assuming the output timezone matches the host.

* fix: start automatic genetic runs in the site timezone

* Preserve aware GENETIC snapshot times across host timezones

* docs: specify site clock and rehearsed merge resolutions

* test: isolate invalid measurement records and refresh API version

* fix: render single-slot genetic tail diagnostics

* docs: refresh schema version after report fix

* fix: preserve configuration-only Optimize API contract

* docs: refresh configuration request schema

---------

Co-authored-by: Christin <info@bikinibottom.capital>
Co-authored-by: Normann <github@koldrack.com>

---------

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
Co-authored-by: Bobby Noelte <b0661n0e17e@gmail.com>
Co-authored-by: r0b2g1t <r0b2g1t@users.noreply.github.com>
Co-authored-by: Normann <github@koldrack.com>
Co-authored-by: Christin <info@bikinibottom.capital>
2026-09-17 20:14:24 +02:00
3c862543a1 feat(devices): add bounded slot physics for GENETIC (#1327)
* feat: adapt configuration for multi optimization algorithms

Decouple configuration from optimization algorithm parameters. Add to_[algorithm]_param() methods
to the configuration that derive optimization algorithm specific parameters from the configuration.
Add x-scope tags to the configuration options that describe for which specific algorithms the
configuration option is for.

The whole device settings are restructured. There are now general settings for the device classes
with the afore mentioned to_[algorithm]_param() methods. The general device settings got their own
directory `devices/settings`. By this the parameter class also does not have to be a pydantic model
which can be used for future optimization/ simulations speed up.

Also the parameter class for a device is now part of the device module. This better decouples and
also is the natural place for parameters of a device.

Besides this feature there are also fixes and improvements:

* feat: extend home appliance time window settings and simulation

  Home appliance can now be configured for multiple runs with per-cycle allowed time windows. The
  number of remaining cycles to plan is determined at runtime by reading the
  ``cycles_completed_measurement_key`` from the measurement store.

* feat: specialiced CycleTimeWindowSequence for time window sequences

  Sequence of time windows associated to cycles.

  This model specializes ``ValueTimeWindowSequence`` so that the ``value``
  field of each ``ValueTimeWindow`` encodes the **cycle index** (0-based
  integer) the window belongs to.

  Typical use: an appliance that must run ``n`` times per day, each run
  constrained to a distinct time window.  Assign ``value=0`` to windows
  for the first cycle, ``value=1`` for the second, and so on.  Multiple
  windows may share the same cycle index (their allowed regions are unioned).
  Windows with ``value=None`` are silently ignored by all cycle-aware methods.

* fix: Make test_configmigrate also regard the _ANY_SENTENIEL in key values

* chore: Make devices configurations a map instead of a list

  This makes config paths stable regardless of declaration order and lets each device settings
  class build its own config path from ``self.device_id`` without needing an external index.
  Tests are adapted likewise.

  Devices configurations are automatically migrated from lists to maps.

* chore: rename levelized_cost_of_storage_kwh to levelized_cost_of_storage_amt kwh

  This better fits in the naming scheme and also makes clear the costs are money.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>

* fix: runtime config update ignored by config file

Runtime settings were handed back to pydantic-settings as init settings,
which rank below the config file and the environment. Any key already
present in EOS.config.json or in the environment silently discarded the
update, so a bulk PUT /v1/config returned 200 without applying anything,
while the granular PUT /v1/config/{path} endpoint kept working.

Add a dedicated runtime settings source ranked directly below the command
line arguments and record granular updates there as well, so both
endpoints share one store that survives re-evaluation of the settings
sources. Environment variables keep precedence over the config file for
all keys that were not set at runtime.

Also repairs revert_settings() and update(), which passed their data
through the same init settings.

Closes #1303

* fix: env vars ignored on first config build

ConfigEOS.__init__ passed self as first positional argument to _setup,
which forwards it to pydantic_settings.BaseSettings.__init__. Its first
positional parameter is _case_sensitive, so the environment source
matched the upper case variable names against the lower case field names
and returned nothing. Environment settings only took effect after the
next configuration setup.

* docs: changelog for config priority fixes

* fix(config): preserve device identities and storage costs during migration

* fix(devices): preserve charge-rate typing and public import compatibility

* ruff format fix

* feat(devices): port slot-aware battery export and direct-use physics

Port scoped device changes from d2e2d58237. Keep PR #1256 parameter conversion structure and separate GENETIC0 devices. Validate physical flows and reprice changed simulation results.

Co-authored-by: Andreas <drbacke@gmx.de>

Co-authored-by: Christin <info@bikinibottom.capital>

* fix(cache): distinguish callables in the shared EMS cache

Include the function object in cache keys so methods of one interpolator cannot reuse a probability as a power value. Cover both call orders, keyword arguments, cache hits and separate closures with identical qualified names.

* fix(devices): constrain the physics port and validate export levels

Defer inactive EV deadline fields to the optimizer port, reject nonfinite export rates, and document the hourly Optimize boundary. Verify converter IDs, rates and LCOS, separate GENETIC0 interpolation, physical boundary flows and independent GENETIC repricing.

* docs(devices): regenerate slot-physics configuration and OpenAPI schemas

* fix(config): satisfy typed device conversion and migration contracts

* docs(config): refresh validated configuration prerequisite schemas

* test(devices): align physics regressions with strict type checking

* docs(devices): refresh API version after prerequisite merge

* docs(interpolator): use portable reStructuredText markup

* docs(devices): refresh API version after docstring compatibility fix

---------

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
Co-authored-by: Bobby Noelte <b0661n0e17e@gmail.com>
Co-authored-by: r0b2g1t <r0b2g1t@users.noreply.github.com>
Co-authored-by: Christin <info@bikinibottom.capital>
2026-09-17 19:14:01 +02:00
AndreasandAndreas 5a3a3bf07e style(pvforecast): apply CI import formatting 2026-09-17 13:53:54 +02:00
AndreasandAndreas 6529c39df4 feat(pvforecast): add calibrated local Akkudoktor backend
Port local PV modeling and outage calibration from feature commits f976335, 6dc58c3 and faed0fd by Andreas. Keep PVForecastAkkudoktor identity and remote default, adapt to async storage, and migrate legacy provider settings.
2026-09-17 13:53:54 +02:00
dr-dimitriandGitHub 7ebe6d714b fix: detect recent Energy-Charts source cadence (#1315)
Infer interval coverage from consistent recent original price spacings while preserving history and forecasting resolution policies. Reuse source data until a successful refresh and cover cadence transitions, fallback, and history repair.

Fixes #1279
2026-09-16 12:22:39 +02:00
1abdd345c4 fix: unify mypy environments for local checks and CI (#1291)
The isolated pre-commit mypy hook previously omitted runtime type information that
make mypy used, hiding errors involving dependencies such as Pydantic and Pendulum.
Makefile, pre-commit and CI now run the same full-project typing policy in the
development environment defined by uv.lock.

- Use uv run --locked --exact --extra dev and the same mypy arguments for Makefile
  and the local hook. Check all of src and tests, including on configuration-only
  changes.
- Pin Python 3.13 for local development and the pre-commit CI job, and install the
  locked pre-commit version in CI.
- Disable incremental analysis because existing Pendulum cache state changes mypy 2.3.1
  diagnostics. Document the policy, the performance tradeoff and the existing typing debt.
- Add a regression test that exercises Makefile, the hook and the CI command in a
  temporary project, accepting valid dependency types and detecting deliberate
  Pydantic/Pendulum assignment errors.

Resolve the newly detected mypy diagnostics.

- Enable the numpydantic and Pydantic mypy plugins, retaining strict Pydantic
  constructor typing with init_typed = true. Validate raw/coercible payloads through model_validate.
- Propagate concrete record, provider and time-window types through generic collections,
  factories and lookup methods. Preserve runtime field inspection and generated time-window
  documentation.
- Align Pendulum annotations with actual factory/arithmetic results while retaining Pydantic
  validation adapters at runtime. Correct optional values, array boundaries, REST handlers
  and plotting interfaces.
- Add pinned scipy-stubs and types-psutil, update uv.lock, and supply the plugins' dependencies.
- Add runtime regression coverage for validated path defaults, normalized time-series metadata,
  generic field inspection, invalid timestamps and unsupported provider imports.

Runtime and compatibility details:

- Validate path defaults as Path objects while retaining raw string defaults needed by
  migration serialization with exclude_defaults.
- Normalize feed-in tariff lists and default charge rates to NumPy arrays; reject missing
  timestamps/uninitialized values explicitly. Importing into a provider without import support
  returns HTTP 400.
- Public JSON schemas and OpenAPI structure match main (excluding the generated version).

Signed-off-by: dr-dimitry

Signed-off-by: dr-dimitry
Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
Co-authored-by: dr-dimitri <87113560+dr-dimitri@users.noreply.github.com>
Co-authored-by: Normann <github@koldrack.com>
2026-09-10 23:20:35 +02:00
NormannandGitHub 25430bd42d fix: infer Energy-Charts coverage from recent source intervals (#1280) 2026-09-06 08:21:48 +02:00
dr-dimitriandGitHub 9d816acbd9 fix: honor Energy-Charts electricity price interval coverage (#1278)
* fix: honor Energy-Charts electricity price interval coverage

* test: make Energy-Charts coverage checks timezone-independent
2026-09-05 15:50:43 +02:00
dr-dimitriandGitHub 940aa1021f fix: honor Energy-Charts feed-in interval coverage (#1275) 2026-09-05 01:05:25 +02:00
7bafc8d02d feat: add PVForecastHomeAssistant provider (#1258)
* feat: add PVForecastHomeAssistant provider

Reads a PV forecast time series directly from a Home Assistant entity
attribute (matching the {"forecast": [{"datetime", "watts"}]} shape
already exposed by common HA PV forecast integrations, e.g. Helios
Forecast and Solcast) and feeds it into pvforecast_ac_power, following
the same self-polling provider pattern as PVForecastVrm.

Closes #1232.

* fix: use MagicMock instead of monkeypatched Response for mypy

requests.Response().json is a typed bound method; reassigning it to a
lambda fails mypy's method-assign check. Use MagicMock(spec=...) instead,
which mocks the response without fighting its static type.

* fix: address PR review on PVForecastHomeAssistant provider

Fixes two issues raised in review on PR #1258:
- _update_data() left stale pvforecast_ac_power values in place when a
  refreshed forecast was empty or shorter than a previous one; it now
  clears the active forecast window before writing and raises instead
  of silently no-op'ing when the response has no usable data.
- pvforecast.homeassistant.entity_id used a "select" widget with no
  manual-entry fallback in EOSdash, leaving it unusable in standalone
  mode where the entity list can't be resolved without SUPERVISOR_TOKEN;
  switched to a plain text field.

Also fills in the config docs and openapi.json for the new
pvforecast.homeassistant.* fields, which were missing from the
original commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix: preserve retained forecast history when clearing stale entries

The previous fix cleared pvforecast_ac_power from start-of-day, but
PredictionProvider deliberately retains historical records back to
keep_datetime (prediction.historic_hours). Since Home Assistant
forecasts are future-only, that clear wiped out retained history
between midnight and the EMS start on every refresh.

Narrow the clear to [ems_start_datetime, end_datetime) - the actual
active forecast window, DST-adjusted - instead of the day boundary.

Addresses review feedback from @NormannK on PR #1258.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Mathias <mathias@Mathiass-MacBook-Air.local>
Co-authored-by: Mathias <mathias@Mathiass-Air.localdomain>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 00:15:40 +02:00
Bobby NoelteandGitHub ba76087db9 feat: electricity fee provider framework and generic providers (#1235)
Add new provider class for electricity fees providers.

Add the generic providers:
- ElecFeeFixed
- ElecFeeImport

The providers provide predictions for:

- elecfee_consumption_amt_wh:
  Total fixed fee for consumed energy per Wh [amount/Wh]. This is the accumulation of all
  fixed per-Wh fees payable on "consumed energy - such as network charge, concession fee,
  and electricity charge - into a single amount.
- elecfee_consumption_percent_amt:
  Total fixed surcharge on consumed energy, given as a percentage of the monetary amount
  already charged for that energy [%]. This is the accumulation of all percentage-based
  surcharges payable on top of the consumed-energy fee - such as VAT - into a single
  percentage. This is a percentage of the fee amount, not a per-Wh rate.
- elecfee_feedin_amt_wh:
  Total fixed deduction from feed-in energy per Wh [amount/Wh]. This is the accumulation of
  all fixed per-Wh charges deducted from feed-in energy - such as metering fees or
  grid-operator handling "charges - into a single amount. Applied after the percentage-based
  deduction, i.e. it reduces the price by a flat amount per Wh rather than by a share of the
  raw price.
- elecfee_feedin_percent_amt:
  Total percentage deducted from the raw feed-in price (spot price) [%]. This is the
  accumulation of all percentage-based deductions payable on the feed-in tariff - such as a
  marketing or balancing fee retained by the aggregator - into a single percentage. It is
  applied as `raw_price * (100 - percent) / 100`, i.e. it scales down the raw price rather
  than adding a surcharge to it.

A new _apply_fee() method is added to the base class for ElecPrice and FeedInTariff to be used to
add the fees in a consistent way. Fees are taken from the active ElecFee provider and applied
to the raw prices given to the _apply_fee() method.

The optional application of fees is added to:

- ElecPriceAkkudoktor
- ElecPriceFixed
- ElecPriceEnergyCharts
- ElecPriceSMARD
- FeedInTariffEnergyCharts
- FeedInTariffFixed
- FeedInTariffSMARD

The import providers ElecPriceImport and FeedInTariffImport do not apply fees by intentention.

The following providers currently do not handle fees defined by ElecFee:

- ElecPriceTibber
- FeedInTariffAkkudoktor
- FeedInTariffDvhubOnline
- FeedInTariffTibber

The tests for this feature are either added or existing tests are extended.

The documentation was extended for the electricity fee provider settings.

Besides this feature further improvements are added:

* feat: add SMARD quarter-hour electricty price and feed-in tariff provider

* feat: to_series method for TimeWindows and ValueTimeWindows

  Additional to to_array the time window sequence can now also produce a pandas series.
  Test have been extended to cover the series generation.

* feat: use time windows in fixed feedin tariff provider

  Feedin tariff can now be configured by time windows - not a single value.

* feat: EOSdash select for PVLib inverters and modules

  Provide PVLib inverter and module names in config selection.

* feat: EOSdash lazy select for big option sets

  Add a new form for lazy selection of big option sets. Filtering and
  generation of the option set is done server-side.

* fix: use raw data for ETS/ median prediction

  Use to raw time series data for ETS/ median prediction to avoid interference
  by e.g. dynamic grid charges.

* fix: EOSdash config drops by type only on details resolve

  Drop configuration by type and path. Prevents dropping of configuration items
  with same type and level but different path.

* fix: EOSdash configuration section closes on update

  Open section if searching or if last update touched this category — including
  updates on deeply nested sub-fields.

* chore: make elecfeefixed, elecpricefixed and feedintarifffixed warn about no windows and default to 0

  Missining configuration creates default 0 value and a warning instead of an exception.

* fix: test setup for providers

  Reset db state on each test run.

* chore: improve config option naming for elecpricefixed.

* chore: adapt elecpricefixed test to changed time_windows naming

* chore: factorized common price provider helpers to priceabc.py

  Factorized common price provider helpers to priceabc.py. Add tests for these helpers.
  Reduce/ change testing of elecpriceabc.py and feedintariffabc.py to cover
  only specifics. Rest of testing is already covered by test_priceabc.py.

* chore: update version

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2026-08-23 02:26:16 +02:00
NormannandGitHub 74d31e2fac fix PVLib mountingplace None handling (#1220)
Handle an explicit None value for pvforecast.planes[].mountingplace before calling .lower().
2026-08-07 15:26:03 +02:00
NormannandGitHub 682e0ccc2b fix PVLib inverter error message (#1219) 2026-08-07 13:55:26 +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 1905682113 chore: adapt pdf visualization (#1205)
Change PDF visualization to be created on demand and per optimization algorithm. The PDF
for the GENETIC0 optimization is provided by the /visualization_results.pdf endpoint.
There is no change in the interface.

By this the optimization algorithm is offloaded from the PDF generation which spares some
time.

To cope with several users may call the /visualization_results.pdf endpoint at the same
time the PDF is generated on the fly without any intermediate file taking the stored
GENETIC0 solution as an input. SVG picture generation is removed as this would again
create intermediate files. Chart pictures can easily be taken from the PDF.

To allow on demand creation of the optimization results visualization the optimisation
solution stored is extended by several new attributes. To keep the deprecated
/optimize endpoint compatible the optimization solution is stripped to the legacy
content before returned. Due to the extension of the solution the optimization tests were
adapted to cover the extended content.

The optimization tests are adapted to test the generated visualization report by
the pypdf reader. Pypdf is added to the development dependencies.

Besides the adaptation several fixes and improvements are added:

* feat: extend /v1/prediction/series endpoint by resampling and filling

  Add parameters for resampling and filling. Add the processing parameter
  to control wether raw data or resampled data shall be returned.

* feat: extend /v1/measurement/series endpoint by resampling and filling

  Add parameters for resampling and filling: Add the processing parameter
  to control wether raw data or resampled data shall be returned.

* feat: standardize and improve API error response

  Use FASTApi exception handlers to provide a standardized API exception handling.
  All exceptions are logged.

  Exception traces are only returned if the new logging configuration parameter
  logging.api_logging_level is set to "DEBUG" or "TRACE". Avoids unwanted leackage
  of server internals on exceptions.

* fix: align to intervall when resampling

  Ensure resampling is aligned to interval also when the buckets are shifted due to the
  align_to_intervall parameter is set.

* chore: make dropna mandatory and default to True

* chore: refactor key_to_xxx data management methods

  Make key_to_series the central method for data resampling and fill.
  Add a new key_to_raw_series to retrieve the data as it is stored
  (without resampling and filling).

  Users of key_to_series were mostly moved to key_to_raw_series as this resembles
  the former interface. Especially in predictions and tests this was done.

* chore: create test data sub-directory for each optimization algorithm

  To prevent cluttering the test data directory and ease test data management for
  optimization algorithms each algorithm got it's own sub-directory. The current
  test data was moved to these sub-directories.

* chore: update version

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2026-08-01 12:45:19 +02:00
Bobby NoelteandGitHub 8344974c16 chore: adapt deprecated endpoints to 15-minutes predictions (#1195)
Bump Version / Bump Version Workflow (push) Canceled after 0s
CodeQL Advanced / Analyze (actions) (push) Canceled after 0s
CodeQL Advanced / Analyze (python) (push) Canceled after 0s
docker-build / platform-excludes (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Run Pytest on Pull Request / test (push) Canceled after 0s
docker-build / build (push) Canceled after 0s
docker-build / merge (push) Canceled after 0s
Ensure the deprecated endpoints get /strompreis, post /gesamtlast, get /gesamtlast_simple,
get /pvforecast to work on 1-hour intervalls even if the prediction provides 15-minutes
intervall data. This keeps the interface compliant to the legacy functionality.

Besides this adaptations several other improvements and fixes are included in this PR.

* feat: extend data management method key_to array by resample_method

  One can now define the resample method on how to aggregate the values in an interval
  for resampling. Three methods are provided:

  - "first": Use the first value in each interval.
  - "mean": Compute the arithmetic mean of all samples in each interval.
  - "interval_mean": Compute the time-weighted mean assuming each value remains valid
     until the next timestamp (piecewise-constant signal).

* feat: extend the get /v1/prediction/dataframe endpoint with resampling parameters

  Make all parameters for resampling available at the endpoint.

* feat: extend the get /v1/prediction/list endpoint with resampling parameters

  Make all parameters for resampling available at the endpoint.

* feat: add new delete /v1/prediction/range endpoint

  The endpoint allows to delete prediction values for a given time span.

* fix: adapt for PVForecastAkkudoktor server side cache handling

  /api.akkudoktor/forecast does it's own caching on requests. Call it with slightly
  randomized request values to avoid getting cached values in the case we need fresh
  data. The requests are anyway rate limited to one request per hour on our side.

* chore: add core.types

  This module centralizes reusable type definitions shared across multiple
  packages. Defining common types here avoids duplication of complex type
  annotations (such as Literal aliases), ensures consistent typing across the
  code base, and helps prevent circular import dependencies between modules.

* chore: extend cache testing

* chore: add system test for deprecated /strompreis endpoint

* chore: add unit test module for server endpoints

  Add a new test module to do unit tests on server endpoints. First test added
  for deprecated get /strompreis endpoint.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2026-07-30 19:54:05 +02:00
Bobby NoelteandGitHub 52fe489d4e feat: add dvhubonline feed-in tariff provider (#1193)
Bump Version / Bump Version Workflow (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (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
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Fetches GET /api/prices?start&end&zone (15-min slots, EUR/MWh) and stores
the raw market price as feed_in_tariff_wh (EUR/Wh) — no import charges/VAT.

Slots beyond the day-ahead horizon are left to the consumer's forward-fill
(FeedInTariffImport behaviour).

5 unit tests + opt-in live smoke (EOS_DVHUB_ONLINE_LIVE=1, passing).

Signed-off-by: Christin <info@bikinibottom.capital>
Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2026-07-29 22:21:17 +02:00
Bobby NoelteandGitHub e23bb7b497 chore: prepare for update of genetic algorithm (#1190)
Bump Version / Bump Version Workflow (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (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
Andreas will update the genetic algorithm for 15-minutes optimization
intervals.

Copy the current GENETIC optimization algorithm to GENETIC0 to enable
to keep the algorithm with the current functionality. Also copy resources
like the load interpolator to the GENETIC0 algorithm to keep them despite
possible later changes to the interpolator.

Make the deprecated legacy /optimize endpoint use the GENETIC0 optimization
algorithm to in-fact behave the same way even if there will later be changes
to the GENETIC algorithm by Andreas. Add a new REST endpoint to provide
the unprocessed optimisation results of the GENETIC and GENETIC0 algorithm
in case one wants to use them as done with the deprecated /optimize endpoint.

Adapt the optimization configuration to have distinct configurations for the
GENETIC and the GENETIC0 algorithm.

Create a copy of the current tests for the GENETIC algorithm to be used
for the GENETIC0 algorithm. This avoids the tests for the GENETIC0
algorithm to be influenced by later changes by Andreas.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2026-07-29 12:56:08 +02:00
Bobby NoelteandGitHub 7e5aa2f218 feat: add Tibber feed-in tariff provider (#1189)
Bump Version / Bump Version Workflow (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (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
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
The `FeedInTariffTibber` provider requests `priceInfo` and `priceInfoRange` with
`resolution: QUARTER_HOURLY` and preserves the native 15-minute timestamps. It uses Tibber's
`energy` spot-price component without the `tax` part or EOS electricity-price charges. The
end-customer `total` component is deliberately ignored.

The provider deliberately rejects hourly API responses instead of silently repeating them. It
reuses `elecprice.tibber.access_token` and `elecprice.tibber.home_id`, so no duplicate credentials
are needed.

Signed-off-by: Andreas Schmitz <akkudoktor.net>
Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2026-07-24 15:52:26 +02:00
Bobby NoelteandGitHub 4c8a8e40f5 feat: add Akkudoktor feed-in provider (#1187)
Bump Version / Bump Version Workflow (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (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
The `FeedInTariffAkkudoktor` provider uses raw day-ahead market prices from
`https://api.akkudoktor.net/prices` as `feed_in_tariff_wh`. It does not add electricity import
charges or VAT. Published prices are extended to the configured prediction horizon with the same
seasonal ETS or median fallback used by the Akkudoktor electricity-price provider.

The Akkudoktor endpoint currently forwards hourly market prices from aWATTar. With a 15-minute
optimization interval, EOS holds each hourly price constant for its four quarter-hour slots. This
keeps the slot grid consistent but does not create genuine quarter-hour market prices.

Signed-off-by: Andreas Schmitz <akkudoktor.net>
Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2026-07-23 22:37:38 +02:00
Bobby NoelteandGitHub ed61918fe0 feat: add EnergyCharts feed-in tariff provider (#1165)
Bump Version / Bump Version Workflow (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (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
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
The `FeedInTariffEnergyCharts` provider uses the raw Energy-Charts day-ahead market price as the
feed-in tariff. It stores prices in `feed_in_tariff_wh` without adding electricity import charges
or VAT. The data is loaded from the Energy-Charts `/price` endpoint for the configured bidding
zone. The native Energy-Charts resolution, including quarter-hour data, is retained.

Energy-Charts usually supplies prices only for the published day-ahead period. If that data does
not cover the complete configured prediction horizon, the provider extends it as follows:

- With more than 800 hours of history, an ETS (Holt-Winters exponential smoothing) forecast with
  weekly seasonality is used.
- With more than 168 hours of history, an ETS forecast with daily seasonality is used.
- With less history, the median of the available values is used as a constant fallback.

The seasonal periods are adjusted to the source resolution. For example, quarter-hour data uses
four values per hour. Values already supplied by Energy-Charts are kept unchanged; only missing
future slots after the last published price are forecast. Consequently, a 15-minute optimization
uses four forecast values per hour without converting them to hourly averages.

Signed-off-by: Andreas Schmitz <akkudoktor.net>
Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2026-07-23 15:48:29 +02:00
Bobby NoelteandGitHub 641873d867 fix: akkudoktor api requests (#1181)
Bump Version / Bump Version Workflow (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (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
The akkudoktor api for PV forecast seems to be  changed and does not support to
concatenate several planes into one request anymore.

Make a request for every plane and add up the power results. Do not use the
new 15 minutes slots as the returned data does not include the hourly values
for windspeed and temperature as does the hourly slots.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2026-07-20 21:46:01 +02:00
Bobby NoelteandGitHub 3bb0e02aed fix: provider settings in top level field of configuration (#1161)
Configuration for some providers was given in the sub-field `provider_settings`
combining the settings of several providers. Pydantic does not understand
this very well and the configuration became cumbersome, especially in EOSdash.

All provider settings from the `provider-settings` sub-field are now lifted
to the top level field of the configuration.

The changes are automatically migrated in the configuration.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2026-07-20 14:37:03 +02:00
Bobby NoelteandGitHub cc8193216a feat: add tibber electricity price provider (#1160)
Signed-off-by: Andreas Schmitz <akkudoktor.net>
Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2026-07-20 13:43:43 +02:00
Bobby NoelteGitHubMilo @KeloYuan
8bf798daeb fix: bare except (#1159)
Bare `except:` catches SystemExit and KeyboardInterrupt, which can:
- Mask Ctrl+C handling during long optimization runs
- Hide critical system signals
- Make debugging harder

Replacement by `except Exception:` preserves the same error handling while respecting
system-level exceptions.

Co-authored-by: Milo @KeloYuan

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2026-07-20 01:11:33 +02:00
6933b33542 feat: rename genetic optimization API fields to English (#675)
Bump Version / Bump Version Workflow (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (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
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Rename the German field names of the genetic optimization API to
English with full backward compatibility:

- English names are canonical and documented in the OpenAPI schema;
  the German names are still accepted on input via validation aliases
  and re-emitted in responses as deprecated computed fields
- fix visualization receiving the raw German-keyed simulation dict
  (KeyError: load_wh_per_hour)
- rename internal German identifiers (optimize_ems, total_balance,
  battery_residual_value, self_consumption, extra_data keys)
- use amount instead of currency/euro in chart labels and schema
  descriptions; document currency handling (whole currency units,
  never cents) in the API description
- regenerate openapi.json and generated docs

Co-authored-by: Tobias Welz <tobias.wizneteu@gmail.com>
2026-07-18 08:08:25 +02:00
75548990e1 feat: add cloud PV forecast providers: pvnode.com, forecast-solar and solcast (#1150)
Bump Version / Bump Version Workflow (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (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
This PR adds three native PV power forecast providers, giving operators more
cloud forecast sources to choose from via pvforecast.provider, alongside the
existing PVForecastAkkudoktor, PVForecastVrm and PVForecastImport:

    PVForecastPVNode — native 15-minute forecasts from the pvnode.com
    V2 API. Saved-site mode (GET /v2/forecast/{site_id}) where the operator enters their API key +
    site id, or inline mode (POST /v2/forecast/inline) using the configured planes.
    PVForecastForecastSolar — the free Forecast.Solar API (no key
    required for the public endpoint). Multi-plane systems issue one request per plane and the
    instantaneous powers are summed per timestamp.
    PVForecastSolcast — the Solcast rooftop-site API (API key + resource id).

Implementation notes

    All three populate the existing pvforecast_ac_power (and mirror pvforecast_dc_power)
    prediction keys, so they slot into the optimizer unchanged.
    Timezone handling: pvnode and Forecast.Solar return site-local wall-clock timestamps with an
    IANA timezone field, which are resolved to absolute instants before resampling; Solcast returns
    UTC period_end and is normalised to the period start.
    Each provider follows the existing provider pattern (provider_id(), _request_forecast() with
    @cache_in_file, _update_data()), is registered in pvforecast.py and prediction.py, and is
    upstream-neutral.

Validation

    27 unit tests (timezone resolution, null/zero handling, kW→W and period-start conversion,
    azimuth conversion, multi-plane summation, request URL/auth, HTTP-error handling).
    ruff check (F/D/S/bandit) and ruff format clean.
    Each provider was additionally validated against its live API with a real plant, confirming
    the response shapes (pvnode: 288 native 15-min slots; Forecast.Solar: instantaneous watts;
    Solcast: kW estimates with period_end/PT30M).

Documentation

    Provider descriptions and configuration examples added to docs/akkudoktoreos/prediction.md.
    CHANGELOG.md entry under Unreleased.
    Regenerated docs/_generated/configpvforecast.md and configexample.md.

Notes for reviewers

Forecast.Solar's free endpoint is rate-limited (12 req/hour) and Solcast's free tier limits daily
calls; both providers rely on the standard 1-hour cache_in_file TTL to stay within budget.

Authors:

The code is created by Christin. Only minor adaptions by Bobby.

Signed-off-by: Christin <info@bikinibottom.capital>
Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
Co-authored-by: Christin <info@bikinibottom.capital>
2026-07-17 16:51:00 +02:00
Bobby NoelteandGitHub 0306e7e4ed fix: prediction import with pydantic-typed bodies (#1152)
Bump Version / Bump Version Workflow (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (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
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Respect pydantic model in serializing json data for import.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2026-07-16 17:15:10 +02:00
Bobby NoelteandGitHub eb9e966de9 fix: move data management to async (#1015)
FAstAPI is an async framework. Data may be imported and exported, load and save, set and get
asynchronously. Prevent interleaving data operations to corrupt the data. In the previous design
sync and async data access was intermixed leading to data corruption.

The basic data classes DataSequence and DataContainer and the derived classes like Provider and
Measurement now are async. Data access is protected by several async locks.

To support the async design of the data classes the database interface became async.

The energy management is also adapted to the new async design. Optimization is still off-loaded
to another thread, but the prepration for the optimization and the post optimization actions now
follow the async design.

Adapter operations are now also protected by async locks.

Tests were adapted to the async design and new tests were created.

Besides this major fix several other improvements and fixes are included in this PR.

* fix: key_to_dict/list/array only regard data records with key value set.

  Before the exclusion of no value data records was only done if the dropna flag was set.

* fix: test for visual result pdf generation

  Due to updates in the library the generated charts text was a little bit different.
  Adapt the test to create the comaprison pdf in the test data durectory and
  update the reference pdf.

* chore: Remove MutableMapping from DataSequence and DataContainer.

  Mutable Mapping does not fit to the now async design.

* chore: Add NoDB database backend

  This backend implements the full database backend interface but performs
  no actual persistence. It is intended for configurations where database
  persistence is disabled (`provider=None`).

* chore: Improve measurement data import testing with real world scenarios.

  Added two new endpoints to support testing.

* chore: Add mermaid to supported documentation tools

* chore: Add documentation about async design

* chore: Add documentation about generic data handling

  Covers the basics of measurement and prediction time series data handling.

* chore: Add empty lines around markdown lists.

* chore: sync pre-commit config to updated package versions

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2026-07-15 16:38:53 +02:00
6f28022ed0 fix: optimization fail after restart (#1007)
Bump Version / Bump Version Workflow (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (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
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Fix documentation for the loadforecast_power_w key.

Fix documentation to explain the usage of import file/ JSON string to
primarily initialise prediction data.

Fix code scanning alert no. 6: URL redirection from remote source

Enable to automatically save the configuration to the configuration file
by default, which is a widespread user expectation.

Make the genetic parameters non optional for better pydantic compliance.

Update:
- bump pytest to 9.0.3
- bump pillow to 12.2.0
- bump platformdirs to 4.9.6
- bump typespyyaml to 6.0.12.20260408
- bump tzfpy to 1.2.0
- bump pydantic to 2.13.0
- bump types-requests to 2.33.0.20260408

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
Co-authored-by: Normann <github@koldrack.com>
2026-04-15 08:48:56 +02:00
Bobby NoelteandGitHub 71e5abce88 fix: energy charts bidding zone in request (#948)
Bump Version / Bump Version Workflow (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (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
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Ensure that the bidding zone in the request is correctly set to a
string value (not an enum).

This seems to be also an issue with python version < 3.11. Add safeguards
to only use python >= 3.11. Still keep a regression test for the enum
conversion to string.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2026-03-15 13:32:05 +01:00
Bobby NoelteandGitHub 8a9aec6d57 feat: add openmeteo weather provider (#939)
Add OpenMeteo to the selectable weather prediction providers.

Also add tests and documentation.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2026-03-13 12:23:21 +01:00
Bobby NoelteandGitHub cf477d91a3 feat: add fixed electricity prediction with time window support (#930)
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
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Add a fixed electricity prediction that supports prices per time window.
The time windows may flexible be defined by day or date.

The prediction documentation is updated to also cover the ElecPriceFixed
provider.

The feature includes several changes that are not directly related to the
electricity price prediction implementation but are necessary to keep
EOS running properly and to test and document the changes.

* feat: add value time windows

    Add time windows with an associated float value.

* feat: harden eos measurements endpoints error detection and reporting

    Cover more errors that may be raised during endpoint access. Report the
    errors including trace information to ease debugging.

* feat: extend server configuration to cover all arguments

    Make the argument controlled options also available in server configuration.

* fix: eos config configuration by cli arguments

    Move the command line argument handling to config eos so that it is
    excuted whenever eos config is rebuild or reset.

* chore: extend measurement endpoint system test

* chore: refactor time windows

    Move time windows to configabc as they are only used in configurations.
    Also move all tests to test_configabc.

* chore: provide config update errors in eosdash with summarized error text

    If there is an update error provide the error text as a summary. On click
    provide the full error text.

* chore: force eosdash ip address and port in makefile dev run

    Ensure eosdash ip address and port are correctly set for development runs.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2026-03-11 17:18:45 +01:00
04420e66ab fix: Improve provider update error handling and add VRM provider settings validation (#887)
Bump Version / Bump Version Workflow (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
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
* fix: improve error handling for provider updates

Distinguishes failures of active providers from inactive ones.
Propagates errors only for enabled providers, allowing execution
to continue if a non-active provider fails, which avoids unnecessary
interruptions and improves robustness.

* fix: add provider settings validation for forecast requests

Prevents potential runtime errors by checking if provider settings are configured
before accessing forecast credentials.

Raises a clear error when settings are missing to help with debugging misconfigurations.

* refactor(load): move provider settings to top-level fields

Transitions load provider settings from a nested "provider_settings" object with provider-specific keys to dedicated top-level fields.\n\nRemoves the legacy "provider_settings" mapping and updates migration logic to ensure backward compatibility with existing configurations.

* docs: update version numbers and documantation

---------

Co-authored-by: Normann <github@koldrack.com>
2026-02-26 18:31:47 +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 39973bf836 fix: load prediction adjustment with measurement in kwh (#826)
Use load energy meter reading in kWh for load prediction adjustment.
Before the reading was falsely regarded to be in Wh.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2026-01-01 12:26:29 +01:00
Bobby NoelteandGitHub 58d70e417b feat: add Home Assistant and NodeRED adapters (#764)
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.

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

* fix: 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<hash>.
  Hash is only digits as expected by home assistant. Development version
  is appended by .dev as expected by docker.

* fix: use mean value in interval on resampling for array

  When downsampling data use the mean value of all values within the new
  sampling interval.

* fix: 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.

* fix: import json string

  Strip outer quotes from JSON strings on import to be compliant to json.loads()
  expectation.

* fix: default interval definition for import data

  Default interval must be defined in lowercase human definition to
  be accepted by pendulum.

* fix: clearoutside schema change

* feat: 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.

* feat: 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.

* feat: 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.

* feat: add duration to string conversion

  Make to_duration to also return the duration as string on request.

* chore: 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.

* chore: 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.

* chore: improve EOSdash config tab display

  Improve display of JSON code and add more forms for config value update.

* chore: 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.

* chore: add home assistant add-on development environment

  Add VSCode devcontainer and task definition for home assistant add-on
  development.

* chore: improve documentation
2025-12-30 22:08:21 +01:00
Bobby NoelteandGitHub 4c2997dbd6 feat: add bidding zone to energy charts price prediction (#765)
Energy charts supports bidding zones. Allow to specifiy the bidding zone in the configuration.

Extend and simplify ElecPrice configuration structure and setup config migration to automatically
update the configuration file.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
2025-11-16 13:26:18 +01:00