-
Notifications
You must be signed in to change notification settings - Fork 1
Configuration
mimirheim is configured from a single YAML file passed via --config. All fields are validated by Pydantic at startup; an invalid config prints a human-readable error and exits.
The complete auto-generated field reference is at Config-Mimirheim. This page provides annotated examples and explains the non-obvious design decisions.
- Minimal example
- Device coverage
- Topic naming and overrides
- Battery configuration in depth
- PV arrays
- EV chargers
- Static and deferrable loads
- Thermal devices
- Strategy and objectives
- Readiness settings
- Home Assistant integration
- Debug dumps
All MQTT topics are derived from mqtt.topic_prefix automatically. You only need to specify the physical device parameters.
mqtt:
host: localhost
port: 1883
client_id: mimir
topic_prefix: mimir
grid:
import_limit_kw: 25.0
export_limit_kw: 25.0
batteries:
home_battery:
capacity_kwh: 13.5
min_soc_kwh: 1.4
charge_segments:
- power_max_kw: 5.0
efficiency: 0.95
discharge_segments:
- power_max_kw: 5.0
efficiency: 0.95
wear_cost_eur_per_kwh: 0.005
inputs:
soc:
unit: percent
pv_arrays:
roof_pv:
max_power_kw: 8.0
static_loads:
base_load: {}mimirheim supports the following device types. Any combination may be used; each type may have multiple named instances.
Maturity note —
batteriesandpv_arraysare the most thoroughly tested and deployed device types. All other device types are implemented and unit-tested but have seen limited real-world deployment. If you use EV chargers, hybrid inverters, deferrable loads, or any thermal device in production, feedback and bug reports are very welcome. Volunteers willing to contribute integration tests or golden-file scenarios are especially appreciated.
| Section | Device type | Role in model |
|---|---|---|
batteries |
Home battery | Charge/discharge storage |
pv_arrays |
PV array | Fixed generation (forecast input) |
ev_chargers |
EV charger | Charge-only or V2H (Vehicle-to-Home) |
hybrid_inverters |
Hybrid inverter | Battery + directly coupled PV |
static_loads |
Fixed household load | Fixed demand (forecast input) |
deferrable_loads |
Washing machine, dishwasher | Scheduled to the cheapest window |
thermal_boilers |
Immersion heater / heat pump DHW | DHW tank optimisation |
space_heating_hps |
Space heating heat pump | Space heating demand scheduling |
combi_heat_pumps |
DHW + SH heat pump | Combined DHW and space heating |
The grid section is required and defines the physical connection limits. There is exactly one grid connection per mimirheim instance.
Every input and output topic has a default derived from mqtt.topic_prefix. In a standard single-instance deployment you do not need to set any topic fields explicitly.
Example: with topic_prefix: mimir and a battery named home_battery:
| Config field | Derived topic |
|---|---|
batteries.home_battery.inputs.soc.topic |
mimir/input/battery/home_battery/soc |
| Prices | mimir/input/prices |
| Trigger | mimir/input/trigger |
| Schedule output | mimir/strategy/schedule |
| Per-device setpoint | mimir/device/home_battery/setpoint |
Override a topic when the sensor is published by another system under a different name:
batteries:
home_battery:
inputs:
soc:
topic: growatt/battery/soc # override: published by Growatt integration
unit: percentSee MQTT-Topics for the complete derived topic table.
batteries:
home_battery:
capacity_kwh: 13.5 # usable capacity (not nameplate)
min_soc_kwh: 1.4 # 10% reserve; solver will not discharge below thiscapacity_kwh is the usable capacity — not the nameplate figure. Use the value from the inverter's data sheet or the value reported as 100% on the SOC sensor.
min_soc_kwh is a hard constraint. Setting it to zero allows the solver to fully discharge, which maximises revenue but may accelerate degradation. A value of 10–15% of capacity is typical.
inputs:
soc:
unit: percent # most residential inverters
# or:
unit: kwh # preferred when the inverter supports itUse kwh if your inverter publishes an absolute energy value — this avoids the small systematic error that accumulates when capacity_kwh in config drifts from the cell's actual usable capacity as the battery ages.
A battery does not have a single fixed efficiency — it varies with charging power. mimirheim models this as a list of power segments, each with its own efficiency fraction. The solver fills the most efficient segment first when charging.
Single-segment (constant efficiency):
charge_segments:
- power_max_kw: 5.0
efficiency: 0.95
discharge_segments:
- power_max_kw: 5.0
efficiency: 0.95This models a battery that charges and discharges at up to 5 kW with 95% round-trip efficiency. The 5% loss is as heat.
Two-segment (power-dependent efficiency):
charge_segments:
- power_max_kw: 2.5
efficiency: 0.97 # low-power, more efficient
- power_max_kw: 2.5
efficiency: 0.93 # high-power, less efficientThis models a battery with a more realistic power-dependent curve. The solver will charge through the first (more efficient) segment before using the second, unless forced by time pressure.
The total maximum charge power is the sum of all segment power_max_kw values.
wear_cost_eur_per_kwh: 0.005 # EUR per kWh of total throughputThis term is subtracted from the objective for every kWh the battery charges or discharges. It represents battery degradation. Without it, the solver may cycle the battery unnecessarily between flat-rate periods.
A value of 0.005 EUR/kWh is typical for an LFP battery with 3000 full-cycle warranty at 0.15 EUR/cycle-kWh.
capabilities:
staged_power: false # hardware accepts continuous setpoints (default)
zero_exchange: false # no closed-loop zero-exchange mode (default)zero_exchange: true enables a closed-loop mode where the inverter uses its own current transformers to actively hold grid exchange near zero. mimirheim decides whether to assert this mode per step; the inverter performs real-time enforcement. This is separate from the constraints.max_export_kw: 0.0 hard limit, which the solver enforces in the model.
pv_arrays:
roof_pv:
max_power_kw: 8.0 # clamps forecast values above this
# topic defaults to: mimir/input/pv/roof_pv/forecastmax_power_kw is used to clip implausibly high forecast values. It does not constrain the solver — PV is always treated as a fixed input. The forecast is published by pv_fetcher or pv_ml_learner.
For inverters that support active curtailment, enable the relevant capability:
pv_arrays:
roof_pv:
max_power_kw: 8.0
capabilities:
power_limit: true # inverter accepts a continuous kW setpoint
outputs:
power_limit_kw: null # derived: mimir/output/pv/roof_pv/power_limit_kwWith power_limit: true, mimirheim adds PV output as a decision variable (bounded by the forecast) and may curtail production when export would reduce total value, for example under zero-export or net-metering constraints.
ev_chargers:
ev_charger:
capacity_kwh: 60.0 # vehicle battery capacity
min_soc_kwh: 0.0
charge_segments:
- power_max_kw: 11.0
efficiency: 0.92
discharge_segments: [] # empty = no V2H
wear_cost_eur_per_kwh: 0.002
inputs:
soc:
unit: percent
# plugged_in_topic derived to: mimir/input/ev/ev_charger/plugged_inThe EV charger uses the same segment structure as the battery. The vehicle must be indicated as plugged-in on plugged_in_topic for the device to appear in the model.
V2H (Vehicle-to-Home): provide non-empty discharge_segments to enable bidirectional charging. The EV then participates in discharge scheduling just like a battery.
Departure target: supplied at runtime on the SOC topic payload as JSON:
{"soc": 72.5, "target_soc_kwh": 48.0, "window_latest": "2026-03-30T08:00:00Z"}target_soc_kwh is a hard constraint — the solver must reach this SOC by window_latest. If the target cannot be reached before departure, mimirheim logs a warning but still finds the best feasible solution.
static_loads:
base_load: {}
# forecast topic defaulted to: mimir/input/baseload/base_load/forecastA static load is a fixed demand profile known in advance. It must be present and up to date for the power balance to be correct. Use one of the baseload helpers to publish it.
A deferrable load is a device with a fixed power cycle (e.g. a washing machine or dishwasher) that must start within a user-defined window. mimirheim picks the cheapest start time within that window.
deferrable_loads:
dishwasher:
power_profile: [1.8, 0.3, 0.3, 1.2] # 4 steps = 1 hour; kW per step
# topic_window_earliest defaulted to: mimir/input/deferrable/dishwasher/window_earliest
# topic_window_latest defaulted to: mimir/input/deferrable/dishwasher/window_latest
# topic_committed_start_time defaulted to: mimir/input/deferrable/dishwasher/committed_start
# topic_recommended_start_time defaulted to: mimir/output/deferrable/dishwasher/recommended_startHow the window works:
- An HA automation (or the user) publishes the window to
window_earliestandwindow_latestas ISO 8601 UTC strings. - mimirheim finds the cheapest valid start time within the window and publishes it to
topic_recommended_start_time. - When the automation starts the device, it publishes the actual start time to
topic_committed_start_time. - If the committed start is in the past and the cycle is ongoing, mimirheim freezes the power draw in the model rather than re-optimising the start.
See Helper-API for the full deferrable load MQTT contract.
An electric immersion heater or heat-pump DHW unit with a storage tank. mimirheim optimises the heating schedule to maintain tank temperature above the minimum required for domestic hot water demand.
thermal_boilers:
dhw_tank:
capacity_litres: 200.0
min_temp_c: 50.0 # minimum to ensure DHW comfort
max_temp_c: 80.0 # safety cutout temperature
heat_loss_coeff_kw_per_k: 0.003 # tank insulation quality
element_power_kw: 2.0
cop: 1.0 # 1.0 for resistance heater; >1 for HP-based DHW
min_run_steps: 2 # minimum 30 minutes per run (avoids short cycling)
inputs:
# topic_current_temp derived to: mimir/input/thermal_boiler/dhw_tank/temp_cWhen no building thermal model is configured, the HP must deliver a user-supplied heat demand over the horizon.
space_heating_hps:
heat_pump:
elec_power_kw: 5.0
cop: 3.5
mode: on_off # or sos2 for modulating heat pumps
min_run_steps: 4 # 1 hour minimum run time
inputs:
# topic_heat_needed_kwh derived to: mimir/input/space_heating/heat_pump/heat_needed_kwhWhen building_thermal is configured, mimirheim tracks indoor temperature as a decision variable. The solver pre-heats the building when electricity is cheap and avoids running the HP when electricity is expensive, within the comfort band.
space_heating_hps:
heat_pump:
elec_power_kw: 5.0
cop: 3.5
mode: on_off
building_thermal:
thermal_capacity_kwh_per_k: 12.0 # building thermal mass
heat_loss_coeff_kw_per_k: 0.6 # heat loss through envelope
comfort_min_c: 19.0
comfort_max_c: 24.0
inputs:
# topic_current_indoor_temp_c derived to: mimir/input/space_heating/heat_pump/btm/indoor_temp_c
# topic_outdoor_temp_forecast_c derived to: mimir/input/space_heating/heat_pump/btm/outdoor_forecast_cThe BTM requires an outdoor temperature forecast to be published at each solve cycle. A weather service or HA weather integration can supply this. The payload is a JSON array of floats (°C), one value per 15-minute step starting from the current solve time.
A combi heat pump covers both DHW and space heating with a single compressor. DHW and SH are mutually exclusive per time step.
combi_heat_pumps:
combi_hp:
capacity_litres: 200.0
min_temp_c: 50.0
max_temp_c: 80.0
heat_loss_coeff_kw_per_k: 0.003
elec_power_kw: 5.0
cop_dhw: 2.8
cop_sh: 3.5
min_run_steps: 2
inputs:
# topic_current_temp derived to: mimir/input/combi_hp/combi_hp/temp_c
# topic_heat_needed_kwh derived to: mimir/input/combi_hp/combi_hp/sh_heat_needed_kwh
# building_thermal: ... (optional, same structure as space_heating_hps)The active strategy is read from the MQTT topic {prefix}/input/strategy. Publish a retained value to change strategy without restarting mimirheim.
| Strategy | Behaviour | Use when |
|---|---|---|
minimize_cost |
Maximise revenue and minimise import cost. Default. | Dynamic tariff with non-trivial price spread |
minimize_consumption |
Minimise grid import first; maximise revenue within that envelope | Maximising self-sufficiency is the primary goal |
balanced |
Weighted combination of cost and import minimisation | Intermediate between the two extremes |
objectives:
balanced_weights:
cost_weight: 1.0
self_sufficiency_weight: 1.0Only the ratio matters. Set cost_weight: 0.3 and self_sufficiency_weight: 1.0 to bias heavily towards self-sufficiency while still responding to price signals.
Under a net-of-meter tariff, import and export prices are equal and the solver is indifferent between solutions with the same net cost but different exchange magnitudes. Setting exchange_shaping_weight to a small positive value breaks this indifference in favour of lower total exchange:
objectives:
exchange_shaping_weight: 1e-4 # 0.0 (default) = disabledChoose a value orders of magnitude below typical energy prices (0.20–0.35 EUR/kWh). A value of 1e-4 cannot reverse an economically justified decision but consistently selects the lower-exchange solution when the cost is equal.
constraints:
max_import_kw: 10.0 # null = no cap (default)
max_export_kw: 0.0 # 0.0 enforces zero exportThese are hard limits enforced by the solver, independent of strategy. They apply across the full horizon.
mimirheim will not solve until all required forecast topics have data covering at least min_horizon_hours ahead.
readiness:
min_horizon_hours: 1.0 # minimum hours of forecast required to attempt a solve
warn_below_hours: 8.0 # log a warning but proceed when horizon falls below this
max_gap_hours: 2.0 # log a warning when any forecast series has a gap wider than thisWhen a trigger is received but readiness is not met, mimirheim logs a warning naming the specific missing or stale topic(s) and takes no action. The previous retained schedule remains in place.
mimirheim publishes HA MQTT discovery payloads when homeassistant.enabled: true. This creates entities in HA for all outputs and per-device setpoints without manual YAML configuration.
homeassistant:
enabled: true
discovery_prefix: homeassistant # default
device_name: "MIMIRHEIM Optimiser" # displayed in HA device cardHA will poll mimir/status/availability and mark all mimirheim entities as unavailable when mimirheim is offline (handled via MQTT last will).
With discovery enabled, mimirheim also registers text entities for each deferrable load's window topics. This creates an HA UI field where the user or an automation can type the window bounds, and a sensor entity that shows the recommended start time.
The reporting section enables production-grade solve dumps consumed by the Reporter helper. This is independent of debug — both can be enabled simultaneously for different purposes.
reporting:
enabled: true
dump_dir: /data/dumps # must match reporter.scan_dir; absolute path, must be writable
max_dumps: 200 # oldest pairs deleted when limit exceeded
# notify_topic defaults to: mimir/status/dump_availableWhen enabled, mimirheim writes a pair of JSON files after each solve and publishes to notify_topic. The Reporter helper subscribes to that topic and renders the files as HTML reports.
The debug section controls log verbosity and ad-hoc developer dumps. Use this for local inspection; use reporting for the production Reporter workflow.
debug:
dump_dir: /data/dumps # absolute path, must be writable
max_dumps: 50 # oldest pairs are deleted when limit is exceededTo enable the solver-level detail log (constraint prices, variable bounds):
MIMIRHEIM_LOG_LEVEL=DEBUG uv run python -m mimirheim --config config.yamlView the full configuration reference at Config-Mimirheim.
Getting started
Helpers
- Common
- Nordpool
- Zonneplan
- PV Fetcher
- PV ML Learner
- Baseload (Static)
- Baseload (HA)
- Baseload (HA DB)
- Reporter
- Scheduler
- Config Editor
Developer
Reference