The energy still stored when the horizon ends keeps its worth: it replaces
grid imports that are paid for afterwards. Crediting that with a single
price per kWh cannot describe it, because the value is not linear in the
amount stored. The first kWh replaces the most expensive hour that PV
cannot cover, the next one the second most expensive, and once every such
hour is served, further energy replaces nothing.
A scalar has to pick one slope for all of it. High enough for the first kWh
means hoarding a full battery; low enough for the last kWh means running it
empty by the end of the horizon - which is exactly what the previous default
of 0 EUR/kWh did.
terminal_value_mode = AUTO (the new default) builds the curve instead. There
is no forecast beyond the horizon, so its trailing window stands in for the
day that follows: residual load max(load - PV, 0) per slot, priced at its
import price, sorted and accumulated. LCOS is subtracted from every marginal
value so stored energy is not credited twice, and the tail beyond the
residual load is only credited when direct marketing allows an export. The
curve is built once per run; the search only interpolates on it.
The solution reports what a run used as terminal_value, curve included, so
the shape can be inspected instead of guessed. FIXED restores the previous
scalar behaviour.
In a 48 h scenario with two cheap slots at the end, AUTO keeps the battery
at 50 % and credits 3.85 EUR where FIXED with 0 EUR/kWh drains it to empty.
The stored optimization results move accordingly - the objective changed.
Replace the single hourly "dishwasher" home appliance with a list of
flexible consumers (home_appliances). Each consumer defines its load
either as an explicit power profile (energy-preservingly resampled onto
the optimization slot grid, incl. 15-min and non-integer interval ratios)
or the flat consumption_wh/duration_h fallback, and runs ONCE or DAILY
within its time windows and the optimization horizon.
- ConsumerScheduleMode + shared load-definition validation (XOR of
profile/fallback, reject negative/NaN/inf, unique device_id)
- ApplianceGeneLayout: variable appliance gene block (index into
allowed_start_slots), ONCE/DAILY calendar-day based, no snapping
- per-device output: result.home_appliance_energy_wh, appliance_starts
(absolute local times), per-device solution columns and DDBC RUN/OFF
instructions on state transitions only
- deprecate dishwasher/washingstart/Home_appliance_wh_per_hour with
backward-compatible mapping and explicit conflict rejection
- max_home_appliances is now an upper bound only; no demo appliance and
no on/off behaviour
- docs, openapi.json, CHANGELOG and optimize_result_2* fixtures updated;
new tests/test_homeappliance.py covers the mandatory test matrix
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* record battery SOC at the start of the interval for accurate display
* map battery charge rates to GeneticOptimizationParameters and update related logic
Instead of using the EV charge rates, the battery now has its own charge rate defined
in the GeneticOptimizationParameters to better separate those entities.
* separate raw gene values from SOC-clamped op factors
The genetic_*_factor columns in the OptimizationSolution dataframe now
always carry the raw gene values (optimizer intent), while the
battery1_*_op_mode / battery1_*_op_factor columns and FRBCInstruction
operation_mode_factor reflect SOC-clamped effective values that can
actually be executed given the battery's state of charge at each hour.
Adds GeneticSolution._soc_clamped_operation_factors():
- AC charge factor: proportionally scaled down when battery headroom
(max_soc - current_soc) is less than what the commanded factor
would store in one hour; zeroed when battery is full.
- DC charge factor: zeroed when battery is at or above max SOC.
- Discharge: blocked when SOC is at or below min SOC.
Both optimization_solution() and energy_management_plan() pass the
clamped values to _battery_operation_from_solution(), so the plan
instructions the HEMS receives reflect physically achievable targets.
* ensure max AC charge power is only used if defined
* update fixtures for PV suffix + SOC-clamp algorithm changes
* handle None case for home appliance start hour in simulation
if the hour is zero, the the home appliance wont start
* update handling of invalid charge indices in autocharge hours
Seems to be a bug, since all invalid indexes needs to be invalidated (also the first one)
* remove double code in simulation preparation and improve comments
* update version
Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
Co-authored-by: Christopher Nadler <christopher.nadler@gmail.com>