Passing data to EMHASS#
Passing your own data#
In EMHASS we have 4 forecasts to deal with:
PV power production forecast (internally based on the weather forecast and the characteristics of your PV plant). This is given in Watts.
Load power forecast: how much power your house will demand over the configured forecast horizon. This is given in Watts.
Load cost forecast: the price of the energy from the grid over the configured forecast horizon. This is given in EUR/kWh.
PV production selling price forecast: at what price are you selling your excess PV production over the configured forecast horizon. This is given in EUR/kWh.
The forecast horizon is delta_forecast_daily local calendar days (one by default), so across a daylight saving transition it spans 23 or 25 hours rather than a fixed 24. See the delta_forecast_daily configuration entry for the full contract.
The sensor containing the load data should be specified in the parameter sensor_power_load_no_var_loads in the configuration file. As we want to optimize household energy, we need to forecast the load power consumption. The default method for this is a naive approach using 1-day persistence. The load data variable should not contain the data from the deferrable loads themselves. For example, let’s say that you set your deferrable load to be the washing machine. The variables that you should enter in EMHASS will be: sensor_power_load_no_var_loads: 'sensor.power_load_no_var_loads' and sensor.power_load_no_var_loads = sensor.power_load - sensor.power_washing_machine. This is supposing that the overall load of your house is contained in the variable: sensor.power_load. The sensor sensor.power_load_no_var_loads can be easily created with a new template sensor in Home Assistant.
If you are implementing an MPC controller, then you should also need to provide some data at the optimization runtime using the key runtimeparams.
To try a request without replacing the live plan, send the same body to /api/v1/dry-run/<action>: see Dry run.
The valid values to pass for both forecast data and MPC-related data are explained below.
Forecast data at runtime#
It is possible to provide EMHASS with your own forecast data. For this just add the data as a list of values to a data dictionary during the call to emhass using the runtimeparams option.
For example, if using the add-on or the standalone docker installation you can pass this data as a list of values to the data dictionary during the curl POST:
curl -i -H 'Content-Type:application/json' -X POST -d '{"pv_power_forecast":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 70, 141.22, 246.18, 513.5, 753.27, 1049.89, 1797.93, 1697.3, 3078.93, 1164.33, 1046.68, 1559.1, 2091.26, 1556.76, 1166.73, 1516.63, 1391.13, 1720.13, 820.75, 804.41, 251.63, 79.25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}' http://localhost:5000/action/dayahead-optim
Or the equivalent rest_command implementation:
rest_command:
dayahead_optim:
url: http://localhost:5000/action/dayahead-optim
method: post
content_type: application/json
payload: >
{
"pv_power_forecast": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 70, 141.22, 246.18, 513.5, 753.27, 1049.89, 1797.93, 1697.3, 3078.93, 1164.33, 1046.68, 1559.1, 2091.26, 1556.76, 1166.73, 1516.63, 1391.13, 1720.13, 820.75, 804.41, 251.63, 79.25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
}
Or if using the legacy method using a Python virtual environment:
emhass --action 'dayahead-optim' --config ~/emhass/config.json --runtimeparams '{"pv_power_forecast":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 70, 141.22, 246.18, 513.5, 753.27, 1049.89, 1797.93, 1697.3, 3078.93, 1164.33, 1046.68, 1559.1, 2091.26, 1556.76, 1166.73, 1516.63, 1391.13, 1720.13, 820.75, 804.41, 251.63, 79.25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}'
The possible dictionary keys to pass data are:
shared_thermal_tanksa list of shared thermal storage objects (several sources feeding one storage), the manual flat alternative toheat_topology. Each tank carriesid,load_ids(the deferrable-load indices of its sources),volume(m3),start_temperature, per-stepmin_temperatures/max_temperatures, and optionallythermal_loss(kW),draw_off_demand(kWh per step),desired_temperatures,overshoot_temperatureandsense(heatorcool). Each load inload_idsneeds adef_load_config[k]entry of the form{"thermal_source": {...}}with its conversion:efficiency(a fixed factor), orsupply_temperatureorheating_curvepluscarnot_efficiency(a heat pump). This is the schema theheat_topologycompiler produces; writing aheat_topologyis usually simpler. Whenheat_topologyis also set, the compiled topology replaces this list (withextend_deferrable_loads: truethe runtime tanks are kept and the topology’s tanks appended).shared_tank_start_temperaturesa per-storage start-temperature override keyed by storage id, e.g.{"dhw": 48.5}. It is applied after theheat_topologycompile, so it patches manual and compiled tanks alike: the per-run equivalent ofsoc_initorheater_start_temperaturesfor an MPC loop that reads a live tank temperature sensor. Unknown ids and non-numeric values are ignored with a warning.shared_tank_prior_heatper-storage heat already produced but not yet arrived because ofthermal_inertia, keyed by storage id: thermal kWh for each of the last L steps, oldest first, e.g.{"house": [0.8, 1.4]}. The initial condition for the lag under rolling MPC (see heat_topology.md). Absent keeps the cold-start behaviour; a malformed entry is ignored with a warning.pv_power_forecastfor the central (P50) PV power production forecast.pv_power_forecast_p10as an optional conservative P10 companion topv_power_forecast. It must use the same representation (list or timestamped mapping) and the same source timeline as P50. When present, the pair is validated/aligned together andweather_forecast_pv_quantile_biasappliesbias * P10 + (1 - bias) * P50. Omitting the companion leaves the existing P50-only behavior unchanged.load_power_forecastfor the Load power forecast.load_cost_forecastfor the Load cost forecast.prod_price_forecastfor the PV production selling price forecast.outdoor_temperature_forecastfor the outdoor temperature forecast in °C, used by thermal loads. When it is omitted, EMHASS uses the weather-forecast air temperature if available.
Every value in these forecasts must follow the Forecast input contract below.
Passing a forecast as timestamped values#
Instead of a plain list, any of these forecast keys can be passed as an object that maps ISO 8601 timestamps to values. EMHASS aggregates the points to the optimization time step and then holds each value until the next provided point (step interpolation), so you only need to supply a point where the value changes. A point whose timestamp falls before the start of the optimization window is used to anchor the first values of the horizon.
For an external PV percentile pair, supply both pv_power_forecast and pv_power_forecast_p10 as timestamped mappings with the same timestamps. The P10 companion reuses this exact alignment path. A list/mapping type mismatch, list-length mismatch, timestamp mismatch, missing P50, or non-numeric, boolean or non-finite P50/P10 value is rejected explicitly rather than independently shifting or silently dropping the companion. Once the source timelines match, the normal timestamp-mapping hold-last semantics apply through the requested horizon, including the existing backfill of leading horizon steps when the first supplied mapping point is later than the window start.
{
"load_cost_forecast": {
"2024-01-01T00:00:00+01:00": 0.20,
"2024-01-01T06:00:00+01:00": 0.15,
"2024-01-01T18:00:00+01:00": 0.30
}
}
For example, an aligned external P50/P10 PV pair can be supplied directly:
{
"weather_forecast_pv_quantile_bias": 0.5,
"pv_power_forecast": {
"2026-09-16T00:00:00+10:00": 0,
"2026-09-16T06:00:00+10:00": 1200,
"2026-09-16T12:00:00+10:00": 5200
},
"pv_power_forecast_p10": {
"2026-09-16T00:00:00+10:00": 0,
"2026-09-16T06:00:00+10:00": 700,
"2026-09-16T12:00:00+10:00": 3600
}
}
Forecast input contract#
This section is the reference for externally supplied forecast values. Other pages link here instead of repeating it. It has three separate layers: the numerical validity of the values you send, how their representation is aligned to the optimization grid, and the physical domain the optimizer finally receives.
Forecast key |
Unit |
External numerical contract |
Optimizer-facing physical domain |
|---|---|---|---|
|
W |
finite real numbers |
|
|
W |
finite real numbers; same representation and timeline as |
blended with P50 by |
|
W |
finite real numbers |
|
|
currency/kWh |
finite real numbers |
signed: negative prices are valid |
|
currency/kWh |
finite real numbers |
signed: negative prices are valid |
|
°C |
finite real numbers |
signed: sub-zero temperatures are valid |
Numerical validity#
Every supplied value must be a finite real number. The following are invalid for every forecast key:
NaN,Infinityand-Infinity;nulland other non-numeric values such as strings ("unavailable","12.5");booleans (
true/false), even though some languages treat them as 1/0.
Invalid input fails the optimization cycle. EMHASS logs one error that names the forecast key, the reason, and the first offending value with its list position or source timestamp, and the action stops before the optimizer runs. It does not silently fall back to the configured forecast method, to the weather-forecast temperature, or to zero. The sign of a value is not part of this check: negative prices and temperatures are valid.
Absence is different from rejection: when outdoor_temperature_forecast is omitted, the existing weather-forecast air-temperature fallback remains available. If the key is supplied but one of its values violates the numerical contract, that cycle fails instead of using the weather fallback.
For compatibility with existing runtime callers, a forecast value that is a string containing a list literal (for example "[1, 2, 3]") is normalized to a list before length and numerical validation. This compatibility does not make strings valid forecast values: a parsed list such as [1, "2.5", 3] is rejected because "2.5" is still a string member.
Plain lists#
A plain list is positional: the first value is the first step of the optimization grid (set by method_ts_round, from the clock read once at the start of the request) and each following value is one optimization_time_step later. The grid is regular in elapsed time, so a daylight saving day needs 23 or 25 hourly values. EMHASS does not resample a list.
A list must contain at least as many values as the forecast horizon. A shorter list is rejected with an error and ignored, so the action uses the configured forecast method instead. It is never padded.
A longer list is accepted, and EMHASS uses its first horizon-length values.
Timestamped mappings#
A mapping of ISO 8601 timestamps to values is aligned onto the optimization grid:
The supplied values are checked against the numerical contract before any aggregation, so an error reports the source timestamp.
Timestamps are normalized by instant. Always include a UTC offset: a timestamp without an offset is read as UTC, not as local time.
Points are aggregated to
optimization_time_stepbuckets using the mean, for example four 15-minute values become one 60-minute value. The buckets are anchored on the first step of the optimization grid (set bymethod_ts_round), so each bucket coincides with one grid step in every time zone.The aggregated points are matched to the forecast grid by UTC instant, so DST transitions do not shift values.
Each value holds until the next supplied point (step, hold-last semantics). Holding applies to the aggregated bucket values: a bucket that contains two points holds their mean, not the later point, over the steps that follow it until the next bucket with a point. A point before the start of the horizon anchors the first steps. Horizon steps before the first supplied point take the first supplied value (backfill).
Coarser source data is therefore held constant, finer data is averaged, and there is no interpolation between points.
Load sign convention and optimizer-facing load#
load_power_forecastis canonical non-negative household consumption in watts. EMHASS never inverts it.load_negativedescribes the sign convention of the load sensor retrieved from Home Assistant (sensor_power_load_no_var_loads). It is applied when that history is prepared and does not apply to a suppliedload_power_forecast.set_zero_minis also a preparation control for retrieved history. It is not applied to supplied forecasts.The load that reaches the optimizer is checked once for every load forecast method (
typical,naive,mlforecaster,csvand runtimelist), after the optional mix with the current measured load. A finite negative value is outside the physical domain of household consumption, so it is clipped to 0 W and one summarized warning reports the count, the minimum and the first affected timestamp. A non-finite or non-numeric value cannot be repaired, so the action fails with an error. A raw ML model prediction is not changed by this check, see the ML forecaster documentation.
Choosing a fallback#
When a forecast source is unavailable, don’t substitute 0 by default. A zero household load or a zero import price is a valid number that produces a mathematically valid but wrong plan. Use a fallback only when it is trustworthy for that quantity, for example 0 W of PV at night or a known fixed tariff. Otherwise skip that optimization cycle, either by not calling the action or by letting EMHASS reject the invalid payload, and run again when fresh data is available.
Step indices and other positional inputs#
Everything that is indexed by optimization step shares the grid of the plain lists: the plain forecast lists above and the step indices start_timesteps_of_each_deferrable_load and end_timesteps_of_each_deferrable_load. Index i is the instant first_step + i x optimization_time_step, measured in elapsed time. On a daylight saving day the local clock labels of those instants skip or repeat an hour.
The first step is not always “now”.
method_ts_rounddecides it, once per request:firstis the step that contains the request time,lastthe next step boundary (the current one if the request is exactly on it), andnearest(default) the closest boundary on the UTC clock, so a request in the second half of a step starts one step later (seemethod_ts_round). Index 0 of a plain list or step index is that first step.One grid per EMHASS request, not per caller. EMHASS reads the clock once when it receives a request and uses that first step for everything in the request. A caller that builds its payload first (a script, a template, a flow) uses its own earlier reading of the clock; if the two readings fall on different sides of a step boundary, the first step the caller assumed and the first step EMHASS used differ by one step. Setting
method_ts_roundtofirstdoes not synchronise the two clocks.Prefer timestamped mappings for forecasts and prices. They are matched by instant, so the source timestamps are kept whatever the first step is (see above). Positional lists and step indices must still correspond to the grid EMHASS actually used: when you must send them, set
method_ts_roundtofirst, floor the clock to the step instead of reproducing thenearestrule, and, before acting on the first step of a time-sensitive plan, check that the first timestamp of the returned plan equals the first step you assumed.Step indices count from the first step.
end_timesteps_of_each_deferrable_load = Nforbids operation at stepNand later;0means no end limit, not “stop immediately”. Do not combine a positive start step with an end of0or a negative end: EMHASS then ignores the start step too (verified with the optimiser), so give the end explicitly (for example the horizon length). A value derived from a clock time must be counted in elapsed time from the first step.Compute elapsed time from instants, not from wall-clock arithmetic. Subtracting or adding to datetimes that share one time zone is a wall-clock operation, so across a daylight saving change the result is an hour off (23 or 25 elapsed hours between the same two local times). In Home Assistant templates use
as_timestamp()(seconds since the epoch), for example(as_timestamp(target) - as_timestamp(now())) / 60for the minutes to a target time, andas_timestamp(first_step) + step_seconds * ifor the instant of stepi.Fixed-length horizons.
prediction_horizonis a number of steps anddelta_forecast_dailyis a number of local calendar days. A 48-step, 30-minuteprediction_horizonis always 24 elapsed hours. Adelta_forecast_dailywindow has a different number of steps only when it contains a daylight saving change, by the size of the change and the step: with 30-minute steps 46 or 50 for a one-hour change (Australia/Sydney), 47 or 49 for a 30-minute change (Australia/Lord_Howe), and 48 once the change is behind the first step. Withoutprediction_horizon,naive-mpc-optimplans only 10 steps.
A Home Assistant template for the first step with method_ts_round set to first and an optimization_time_step of 30 minutes:
{%- set step_s = 1800 -%}
{%- set first_step = (as_timestamp(now()) // step_s) * step_s -%}
The floor on epoch seconds is the local step boundary only when the UTC offset is a whole number of steps, which holds for whole-hour and half-hour offsets with a 30-minute step. For a zone such as Asia/Kathmandu (+05:45) or Pacific/Chatham the floor lands on :15 and :45, so add the offset first: {%- set off = now().utcoffset().total_seconds() -%} and first_step = ((as_timestamp(now()) + off) // step_s) * step_s - off (checked in Home Assistant for Kathmandu, Kolkata, Sydney, Chatham and St. John’s).
Passing other data at runtime#
It is possible to also pass other data during runtime to automate energy management. For example, it could be useful to dynamically update the total number of hours for each deferrable load (operating_hours_of_each_deferrable_load) using for instance a correlation with the outdoor temperature (useful for water heater for example).
A machine-readable schema of the runtime-only optimization parameters is committed at
src/emhass/static/data/runtime_params.json. The test suite checks it against the configuration surfaces. It is not currently used to generate the OpenAPI spec, whose /action/{action_name} request body is a generic JSON object, and it does not describe the forecast keys above.
Here is the list of the other additional dictionary keys that can be passed at runtime:
number_of_deferrable_loadsfor the number of deferrable loads to consider.nominal_power_of_deferrable_loadsfor the nominal power for each deferrable load in Watts.operating_hours_of_each_deferrable_loadfor the total number of hours that each deferrable load should operate.Alteratively, you can pass
operating_timesteps_of_each_deferrable_loadto set the total number of timesteps for each deferrable load. (better parameter to use for setting under 1 hr)
start_timesteps_of_each_deferrable_loadfor the timestep from which each deferrable load is allowed to operate (if you don’t want the deferrable load to use the whole optimization timewindow).end_timesteps_of_each_deferrable_loadfor the timestep before which each deferrable load should operate (if you don’t want the deferrable load to use the whole optimization timewindow).def_current_statePass this as a list of booleans (True/False) to indicate the current deferrable load state. This is used internally to avoid incorrectly penalizing a deferrable load start if a forecast is run when that load is already running.treat_deferrable_load_as_semi_contto define if we should treat each deferrable load as a semi-continuous variable.set_deferrable_load_single_constantto define if we should set each deferrable load as a constant fixed value variable with just one startup for each optimization task.Note: for the per-load array parameters above (and also
deferrable_load_max_cost), a value shorter thannumber_of_deferrable_loadsis padded with that parameter’s default, whether it came from your configuration or from this runtime payload. A runtime-supplied short array also logs a warning naming the parameter and how many entries were added; a config-sourced one stays silent. Pass a full-length array to avoid the warning.solcast_api_keyfor the SolCast API key if you want to use this service for PV power production forecast.solcast_rooftop_idfor the ID of your rooftop for the SolCast service implementation.solar_forecast_kwpfor the PV peak installed power in kW used for the solar.forecast API call.battery_minimum_state_of_chargethe minimum possible SOC.battery_maximum_state_of_chargethe maximum possible SOC.battery_target_state_of_chargefor the desired target value of the initial and final SOC.battery_discharge_power_maxfor the maximum battery discharge power.battery_charge_power_maxfor the maximum battery charge power.publish_prefixuse this key to pass a common prefix to all published data. This will add a prefix to the sensor name but also the forecast attribute keys within the sensor.current_period_peakthe peak grid import (in Watts) already incurred during the current billing period. Used bynaive-mpc-optimanddayahead-optim, and only when a capacity/demand charge (capacity_cost_per_kw) is configured. A list of K entries whencapacity_cost_per_kwis a list of K rates (issue #540 Part B). See the dedicated section below.
Requiring an intermediate battery SOC target (naive-mpc-optim)#
By default the battery SOC is only pinned at the start (soc_init) and end (soc_final) of the horizon, leaving the optimizer free to choose the trajectory in between. Sometimes you want to guarantee the battery reaches a given charge level partway through the horizon — for example, “make sure the battery is full by the time the cheap window ends, even though it’s fine to discharge it afterwards”. This is the request in issue #553.
Two optional runtime keys (only used by naive-mpc-optim) make this possible:
soc_target: the desired minimum SOC as a fraction in[0, 1](NOT percent). It is clamped to[battery_minimum_state_of_charge, battery_maximum_state_of_charge].soc_target_timestep: the 0-based horizon timestep by whichsoc_targetmust be reached. After this step the battery is free to discharge again. Defaults to the last timestep ifsoc_targetis given without it.
Both default to unset (None), in which case nothing changes — the constraint is a no-op and existing behaviour is preserved. The target is a one-sided constraint (SOC >= soc_target at that step), so it never forces the battery to discharge.
For example, the following runtime payload tells EMHASS that the battery must reach 100% SOC by horizon step index 14 (the SoC after that step, ~7.5 h into a 30-min-step horizon), while still letting it discharge in the steps after that:
{
"prediction_horizon": 48,
"soc_init": 0.30,
"soc_target": 1.0,
"soc_target_timestep": 14
}
As an HA rest_command:
rest_command:
naive_mpc_optim:
url: http://localhost:5000/action/naive-mpc-optim
method: post
content_type: application/json
payload: >
{
"prediction_horizon": 48,
"soc_init": {{ states('sensor.battery_soc') | float / 100 }},
"soc_target": 1.0,
"soc_target_timestep": 14
}
Warning
The target must be reachable by its timestep. If the requested SOC cannot be achieved in time given soc_init, battery_charge_power_max and the optimization time step, EMHASS logs a warning and the LP is likely to be infeasible (the target is a hard constraint, so it is not silently relaxed). Size soc_target / soc_target_timestep to what your charge power actually allows (e.g. don’t ask for +0.7 SOC in 3 steps if a full charge takes 10). Note too that with the default set_nodischarge_to_grid: true a high target can be infeasible if local load is too low to shed the stored energy back down to soc_final. soc_target_timestep is clamped to [0, prediction_horizon - 1].
Batteries that are not always connected: EV / V2G availability windows (naive-mpc-optim)#
With number_of_batteries > 1 one of the batteries can be an electric vehicle on a bidirectional (V2G / V2H) charger. Such a battery is only part of the system while the car is plugged in, it must reach a charge level by the time the driver leaves, and the car itself usually limits how much and how deep it may be discharged. Three optional runtime keys (only used by naive-mpc-optim) model this without changing number_of_batteries between runs, so the problem structure, the optimization cache and the published per-battery sensors stay the same whether the car is there or not:
battery_availability: one entry per battery. Each entry isnull(always available, the default), a number, or a list with one value per timestep. A value is a fraction in[0, 1](not percent: a value outside that range is rejected) that scales that battery’sbattery_charge_power_maxandbattery_discharge_power_maxfor that timestep:1= connected at full power,0= not connected. While a battery is unavailable it neither charges nor discharges, so its SOC stays frozen. A list shorter than the horizon is padded with its last value. With a single battery a flat list is read as its time series.battery_discharge_energy_max: one entry per battery, the maximum energy (Wh, battery side) the battery may deliver over the horizon, ornullfor no cap. Use it for a V2G warranty or cycle budget (for example a car whose manufacturer allows a fixed number of discharged kWh over its lifetime). The cap applies to one optimization run only: EMHASS does not remember what was discharged in earlier runs, so send the budget that is still left on every MPC call. A bare number applies to every battery.soc_target/soc_target_timestep(see the previous section) also accept one entry per battery. A battery with a target but no timestep (null) gets the target at its last available timestep, i.e. the departure. Because the SOC is frozen after departure, also passsoc_finalfor that battery equal to the target, so the end-of-horizon SOC agrees with it (EMHASS logs a warning when it is lower).
A minimum SOC below which the car refuses to discharge is the per-battery battery_minimum_state_of_charge; a lower discharge power accepted by the car is the per-battery battery_discharge_power_max. If a battery starts below its minimum SOC and cannot recover into its band inside its availability window (for instance a car that is not plugged in at all), the minimum is not enforced for it in that run instead of making the problem infeasible. The same holds for a battery above its maximum SOC that cannot come down inside its window or its discharge budget. With set_battery_dynamic the ramp limits also apply around a disconnection, so the optimizer ramps a battery down over the steps before its availability drops to zero.
Example: battery 0 is the home battery, battery 1 a 75 kWh car that is plugged in for the next 72 quarter-hours, must leave at 84 %, may discharge at most 11 kW and at most 10 kWh, and never below 20 %:
{
"number_of_batteries": 2,
"battery_nominal_energy_capacity": [36160, 75000],
"battery_charge_power_max": [17600, 25000],
"battery_discharge_power_max": [19200, 11000],
"battery_minimum_state_of_charge": [0.1, 0.2],
"battery_maximum_state_of_charge": [1.0, 0.9],
"soc_init": [0.78, 0.71],
"soc_final": [0.5, 0.84],
"battery_availability": [null, [1, 1, 1, "... 72 ones, then zeros ..."]],
"soc_target": [null, 0.84],
"battery_discharge_energy_max": [null, 10000]
}
When the car is unplugged, keep number_of_batteries at 2 and send battery_availability: [null, 0] with the car’s last known SOC as soc_init/soc_final; its power forecast is then zero for the whole horizon.
Deferrable loads that are not always available (naive-mpc-optim)#
A deferrable load can have steps in which it must not run, or may run only at reduced power: a heat pump switched to manual mode or outside its operating range, a boiler whose fuel is not released, a charger that is shared with something else. The optional runtime key def_availability (only used by naive-mpc-optim) has the same shape and validation as battery_availability above:
One entry per deferrable load, in the order of
nominal_power_of_deferrable_loads(for aheat_topology, the order of its source-to-storage flows, i.e. the index ofP_deferrable<k>). Each entry isnull(always available, the default), a number, or a list with one value per timestep. With a single deferrable load a flat list is read as its time series.A value is a fraction in
[0, 1](not percent: a value outside that range is rejected) that scales the load’s nominal power for that timestep:1= full power,0= the load must not run. A list shorter than the horizon is padded with its last value.0is a hard limit: it wins over everything that would otherwise force the load on (def_current_statewith a single-constant load, the rest of a minimum on-time,def_current_power). Such a forced run ends at the first step the load cannot run.A semi-continuous load runs at its ON level or not at all, and a sequence load (a list as its nominal power) runs its fixed power profile, so for both any value below
1blocks the step. A fraction is a power ceiling for a continuous load (and for a load with a minimum power, as long as the ceiling stays above that minimum).A sequence load is placed in one piece, so the available steps must leave room for one complete placement of its sequence; a sequence that is already running cannot be interrupted. Otherwise the run stops with an error that names the load.
The operating requirement (
operating_hours_of_each_deferrable_load/operating_timesteps_of_each_deferrable_load) must still fit into the available steps, as it must fit into a window set withstart_timesteps_of_each_deferrable_load/end_timesteps_of_each_deferrable_load.
The mask is a parameter of the optimization problem, so changing it between runs does not rebuild the problem.
Example: two deferrable loads; the second must not run for the first 4 timesteps and only at half power for the next 2:
{
"def_availability": [null, [0, 0, 0, 0, 0.5, 0.5, 1]]
}
Capping the demand charge at the peak already incurred (naive-mpc-optim and dayahead-optim)#
EMHASS can model a capacity / demand charge via the configuration parameter capacity_cost_per_kw (issue #623). With capacity_charge_interval_timesteps = 1 (default), it prices the highest eligible positive-import timestep; with N > 1, it prices the highest eligible completed tariff-interval average described below.
In a real tariff the demand charge is assessed over a billing period, not just the optimization horizon. The billing period may use any caller-defined start/end dates; it does not need to be a calendar month. Once you have already incurred, say, a 7 kW billed peak earlier in the current period, there is no point spending battery energy now to keep this horizon’s eligible peak below 7 kW: that peak is already “locked in” and will be billed regardless. The optional runtime key current_period_peak tells EMHASS what that already-incurred billed peak is, so it only shaves the part of a new peak that is actually above it.
current_period_peak: the billed peak already incurred in this billing period, in Watts (not kW). Withcapacity_charge_interval_timesteps = 1, use the highest eligible positive-import timestep. Withcapacity_charge_interval_timesteps > 1, use the highest eligible completed clocked tariff-interval average; keep the currently open interval incapacity_charge_current_interval_history(MPC only:dayahead-optimcannot represent a partly elapsed interval, so start a day-ahead horizon on an interval boundary) until it completes. Only effective whencapacity_cost_per_kw > 0; ignored otherwise.
It defaults to unset (None, equivalent to 0), in which case the full horizon peak is priced, identical to behaviour without this key. It is a one-sided floor (peak_import >= current_period_peak), so it never forces additional import and never makes the problem infeasible.
For example, with a capacity charge configured and a 7 kW billed peak already incurred in the current billing period:
{
"prediction_horizon": 48,
"soc_init": 0.30,
"current_period_peak": 7000
}
As an HA rest_command, reading a kW sensor that tracks the applicable billing-period peak and converting to Watts (for N > 1, that sensor must track the highest completed tariff-interval average, not the largest instantaneous sample):
rest_command:
naive_mpc_optim:
url: http://localhost:5000/action/naive-mpc-optim
method: post
content_type: application/json
payload: >
{
"prediction_horizon": 48,
"soc_init": {{ states('sensor.battery_soc') | float / 100 }},
"current_period_peak": {{ states('sensor.billing_period_peak_grid_import_kw') | float(0) * 1000 }}
}
Warning
current_period_peak is in Watts, not kilowatts: pass 7000, not 7, for a 7 kW peak (if your sensor is in kW, multiply by 1000 as shown above). It only has an effect when capacity_cost_per_kw is set above 0; on its own it does nothing. It is a floor, not a target: it never raises import or forces a new peak, it only stops the optimizer chasing a peak it cannot reduce. A negative, non-finite (NaN or infinity) or non-numeric value is ignored (treated as 0). Reset it at the start of each billing period (e.g. via a utility-meter cycle) so a stale high value does not suppress legitimate peak shaving in the new period. With dayahead-optim, which has no capacity_charge_window, do not send the old period’s peak when the horizon crosses into a new billing period (for example a plan made on the last day of the month): the floor would make import up to that peak free on the first day of the new period too, and the plan could set a new billed peak there. Omit current_period_peak for such a run, or run naive-mpc-optim with a window.
Restricting the demand charge to a tariff demand window (naive-mpc-optim)#
Many demand tariffs do not charge the highest import of the whole day: they charge the highest import inside a demand window (e.g. 16:00-20:00 on business days only). Without a window, EMHASS prices the peak over every timestep of the horizon, so a deliberate off-window import (say, midday EV charging) pins peak_import and gets taxed for a cost that will never be billed - while shaving inside the actual window has zero marginal value (issues #540 / #623).
The optional runtime key capacity_charge_window masks the peak constraint to the window:
capacity_charge_window: a list of weights in[0, 1]of lengthprediction_horizon, aligned to the horizon timesteps likeload_cost_forecast. Atcapacity_charge_interval_timesteps = 1, the priced peak only sees timesteps where the mask is non-zero (peak_import >= mask[t] * grid_import[t]). Atcapacity_charge_interval_timesteps > 1, the mask is evaluated at each completed tariff interval’s endpoint and scales that completed interval average. Only effective whencapacity_cost_per_kw > 0; ignored otherwise.
EMHASS stays tariff-agnostic: the caller owns the calendar (business days, public holidays, seasons) and simply sends the right mask each cycle. Combined with current_period_peak this models a windowed demand tariff over the caller-defined billing period: the window mask says where a peak can be set, the floor says how high the bar already is.
As an HA rest_command template fragment, marking 16:00-20:00 on weekdays for a 48-step / 30-minute horizon:
{% set nf = now().replace(minute=(now().minute // 30) * 30, second=0, microsecond=0) %}
{% set ns = namespace(mask=[]) %}
{% for i in range(48) %}
{% set dt = nf + timedelta(minutes=30 * i) %}
{% set ns.mask = ns.mask + [1 if (dt.weekday() < 5 and 16 <= dt.hour < 20) else 0] %}
{% endfor %}
"capacity_charge_window": {{ ns.mask | tojson }}
Note
The mask defaults to unset (None) = every timestep priced, identical to behaviour without this key. An all-zero mask (no window in this horizon) makes the demand term a constant - the plan is then identical to running without a capacity charge. An invalid mask (non-numeric entries, NaN/infinity, or shorter than the horizon) is ignored with a warning and the full horizon is priced; a longer mask is truncated; weights outside [0, 1] are clipped. Fractional weights are allowed: at N = 1 they scale that timestep’s import; at N > 1 the endpoint weight scales the completed tariff-interval average, including any realised-history contribution to the first interval. If the horizon crosses the end of the current billing period, zero mask entries that belong to the next billing period. When the boundary is actually crossed, reset current_period_peak for the new period. The billing period may use any caller-defined start/end dates; it does not need to match a calendar month.
Excluding a tariff-eligible occurrence from the current MPC solve’s peak (naive-mpc-optim)#
In a rolling-MPC horizon, two or more occurrences of the same recurring demand window can appear in one solve: a nearer occurrence that is about to be executed and committed as billing history, and a later occurrence that is still fully replannable next cycle. capacity_charge_window alone cannot express this: both occurrences are genuinely tariff-eligible, so masking the later one there would overload eligibility with a second meaning (issue #540).
The optional runtime key capacity_charge_consideration is a separate weight for exactly this case:
capacity_charge_consideration: a list of weights in[0, 1]of lengthprediction_horizon, aligned likecapacity_charge_window. It controls whether an otherwise tariff-eligible prospective timestep/interval participates in this solve’s prospective capacity peak. It can only narrow whatcapacity_charge_windowalready allows - it can never widen it, and it never scales or erasescurrent_period_peak(realised billing history). Only effective whencapacity_cost_per_kw > 0; ignored otherwise.
Note
capacity_charge_consideration defaults to unset (None) = every tariff-eligible timestep/interval considered, identical to behaviour without this key - this reproduces today’s all-occurrences peak exactly. Ordinary usage is expected to be 0/1 (does this occurrence currently count toward the peak, yes or no); it is not a fractional billing discount or a probability, though fractional weights pass through as a prospective-peak influence weight for the same reason capacity_charge_window allows them. Invalid input (non-numeric, NaN/infinity, shorter than the horizon) is ignored with a warning and falls back to full consideration; a longer vector is truncated; weights outside [0, 1] are clipped - the same fail-open convention as capacity_charge_window.
Policy risk is caller-owned. Excluding a later, genuinely tariff-eligible occurrence from consideration does two things, and a caller must plan for both. It removes the MPC’s incentive to pre-position the battery for that occurrence in the current solve; and, because only its capacity contribution was removed while everything else about the timestep is unchanged, it makes that occurrence comparatively attractive as a place to put grid import or battery charging. EMHASS exposes the generic mechanism only - it does not decide, or default to, a “nearest occurrence only” policy. A caller may mark exactly one occurrence as considered, several, or all of them; the choice, and its consequences, belong to whoever builds the consideration vector each cycle.
An excluded timestep is capacity-unpriced in this solve, not free. Consideration only removes a timestep’s contribution to this solve’s prospective capacity peak. The energy price, the battery, grid and deferrable-load constraints and every other objective term still apply there exactly as before, and the timestep remains fully tariff-eligible - the tariff will still measure it. Because the capacity term no longer penalises import at that timestep, the optimizer may shift import or charging into it whenever doing so improves the rest of the objective.
A direct consequence for reading results: peak_import is an internal optimisation variable used to build the capacity term - it is not returned by naive-mpc-optim and is not published. To check what a plan actually does, use the returned P_grid_pos and reconstruct the same tariff metric EMHASS priced (see the demand-charge cookbook’s tariff-metric helper): the eligible native timestep at N=1, or the completed tariff-interval average at N>1. When consideration is active, reconstruct it using the effective capacity_charge_window * capacity_charge_consideration weight, not the raw window alone - otherwise the reconstruction won’t match what this solve actually considered, and can show a large import at an excluded but still tariff-eligible timestep that never raised this solve’s priced peak.
Pricing the tariff’s measurement interval, not the raw timestep#
Most demand tariffs bill the average import power over a fixed clocked interval rather than the largest native optimizer timestep (issue #540).
The three time concepts are independent:
Concept |
Example |
Owner |
|---|---|---|
EMHASS native timestep |
5 minutes |
|
Tariff measurement interval |
30 minutes |
|
Billing period |
14 August to 13 September |
caller/orchestrator |
Set:
capacity_charge_interval_timesteps =
tariff_measurement_interval_minutes / optimization_time_step_minutes
The result must be a positive integer for exact interval representation. Invalid values warn and fall back to 1 (no aggregation). Examples: 15/5 = 3, 30/5 = 6 and 30/30 = 1. A 30-minute tariff with a 20-minute optimizer step gives 1.5 and cannot be represented exactly by this N-timestep aggregation.
At N = 1 (default), the original per-timestep capacity-charge behavior applies. At N > 1, only completed N-timestep tariff-interval averages can raise the priced peak. A single 6000 W 5-minute import inside an otherwise-zero 30-minute (N = 6) interval therefore contributes 1000 W to the completed interval average.
capacity_charge_interval_timesteps is a structural optim_conf parameter shared by naive-mpc-optim, dayahead-optim and perfect-optim. Changing it changes the optimization structure/cache key and rebuilds the problem.
Runtime state for a partly elapsed MPC interval#
capacity_charge_current_interval_history is naive-mpc-optim runtime state. It is a list of the average positive grid-import power in Watts for each native timestep already elapsed in the currently open tariff interval, oldest to newest.
For a 5-minute optimizer and a clocked 30-minute tariff interval, a solve starting at 17:20 has four elapsed native intervals (17:00-05, 05-10, 10-15 and 15-20). Example:
{
"prediction_horizon": 24,
"capacity_cost_per_kw": 8.0,
"capacity_charge_interval_timesteps": 6,
"capacity_charge_current_interval_history": [2000, 4000, 0, 6000]
}
The history length (0 to N - 1) encodes the horizon’s phase inside the open tariff interval. Each entry is an interval average, not an instantaneous sensor snapshot. An equivalent energy-derived value is valid:
average_W = elapsed_energy_Wh / (optimization_time_step_minutes / 60)
If that 17:20 solve plans 3000 W for 17:20-25 and 1000 W for 17:25-30, the completed 17:00-17:30 interval is (2000 + 4000 + 0 + 6000 + 3000 + 1000) / 6 = 2666.7 W. In decision-index terms, with history length m and interval size N = capacity_charge_interval_timesteps, that first completed interval ends at decision index e0 = N - 1 - m (here 6 - 1 - 4 = 1, the second of the two still-planned timesteps above); every subsequent completed interval ends N decision timesteps after that.
When N > 1, current_period_peak must use the same tariff metric: the highest eligible completed clocked tariff-interval average already incurred in the current billing period. Do not add the currently open interval to current_period_peak until that interval completes.
capacity_charge_window still composes with aggregation. For N > 1, it is evaluated at each completed tariff interval’s endpoint, so demand-window boundaries should align with measurement-interval boundaries. The caller owns timezone, season, business-day, holiday and billing-period calendar logic.
capacity_charge_consideration composes the same way: for N > 1 its value is also evaluated at each completed tariff interval’s endpoint, and that combined capacity_charge_window * capacity_charge_consideration weight applies uniformly to the whole completed interval average - the planned portion in the horizon AND, for the first interval, any realised-history contribution from capacity_charge_current_interval_history. Consideration boundaries should therefore also align with tariff measurement-interval boundaries when N > 1: hold the chosen capacity_charge_consideration value constant across every planned native timestep of a given completed interval (decision indices 0..e0 for the still-open first interval, then each following N-sized span) rather than only at the endpoint - the endpoint alone sets the applied weight, but a differing planned span still trips EMHASS’s own misalignment warning. capacity_charge_current_interval_history itself is never rewritten to compensate; it already holds only realised samples.
If the optimization horizon crosses into the next billing period, keep next-period mask entries at 0 until the actual rollover. After rollover, reset current_period_peak and build the new period’s mask. The current single-component model cannot carry separate old-period and new-period incumbent peaks in one solve. With N > 1, exact rollover handling also assumes the billing-period boundary aligns with a tariff measurement-interval boundary; EMHASS does not split one aggregated interval across two billing periods.
Multiple independent capacity/demand components in one solve (issue #540 Part B)#
A bare capacity_cost_per_kw models one capacity/demand-charge component. Some tariffs bill more than one demand peak on the same connection concurrently (for example a seasonal-demand charge and an off-peak-demand charge). Pass capacity_cost_per_kw as a list of K rates to price K such components in the same single optimisation:
{
"prediction_horizon": 24,
"capacity_cost_per_kw": [8.0, 3.0],
"capacity_charge_interval_timesteps": [6, 6],
"current_period_peak": [6000, 4000],
"capacity_charge_window": [[/* 24 weights for component 0 */], [/* 24 weights for component 1 */]],
"capacity_charge_consideration": [[/* K=0 */], [/* K=1 */]],
"capacity_charge_current_interval_history": [[/* K=0 */], [/* K=1 */]]
}
Canonical form.
capacity_cost_per_kwis normalised before anything structural is decided:3.0and[3.0](what the config form serialises for a single value) and[]all collapse to the legacy scalar K=1 form — same model, same cache key, no per-component objects. Only a list of two or more rates selects K>1 (K = len(list)).capacity_charge_interval_timestepsis normalised the same way:6and[6]both mean a shared 6-timestep basis;[6, 3]means genuine per-component bases.Structural vs runtime.
capacity_cost_per_kw(the list, and its length K) andcapacity_charge_interval_timestepsare structuraloptim_conf— a change rebuilds the problem and its cache key.current_period_peakis a runtime input ofnaive-mpc-optimanddayahead-optim;capacity_charge_window,capacity_charge_considerationandcapacity_charge_current_interval_historyarenaive-mpc-optimruntime inputs. All four are runtime inputs — changing them between ticks is a warm-start cache hit.Every runtime input becomes a list of exactly K entries, one per component in
capacity_cost_per_kworder.capacity_charge_interval_timestepsmay be a bare value (shared measurement basis, broadcast to every component) or a list of exactly K for independent per-component intervals — any other list length is a configuration error (a wrong-length interval list would silently redefine the tariff measurement basis, e.g. a 30-minute average-demand charge becoming a native per-timestep peak charge, so it is rejected rather than reinterpreted).Independent component semantics. Each component has its own rate, measurement interval, eligibility window, MPC consideration, realised open-interval history and already-incurred incumbent peak. A peak visible only in component A’s window never raises component B’s peak; A’s consideration mask never affects B; each incumbent floor binds only its own component;
capacity_charge_windowvscapacity_charge_considerationkeep their existing distinct meanings per component (eligibility vs this-solve prospective participation), and consideration never scales or erases any component’s incumbent peak.One optimiser. Still one solve, one physical dispatch, one objective. The capacity term generalises from
capacity_cost_per_kw * peak_import / 1000tosum over k of capacity_cost_per_kw[k] * peak_import[k] / 1000.A component with rate
0(or invalid) is economically inactive — nopeak_importvariable, no capacity epigraph, no objective term; its inert indexed window/incumbent Parameter slots may still exist.[0.0, 0.0]is a valid, inert K=2 configuration whose plan matches running with no capacity charge.A bare scalar
capacity_cost_per_kwis unchanged: it is the K=1 charge and every existing single-component config behaves identically.A non-list or wrong-length runtime value (e.g. 3 masks for a K=2 charge, or a bare scalar
current_period_peak) is dropped for every component with one warning — never partially applied.Caller-owned policy is unchanged. EMHASS never sees a tariff name, calendar, season or clock time for any component — the caller builds each component’s numeric masks. The consideration/myopia trade-off documented above applies per component.
dayahead-optim and perfect-optim also aggregate when N > 1, but they do not receive elapsed-interval history. Their horizon start must therefore align to a tariff measurement-interval boundary.
A tariff interval incomplete at the far end of the horizon is not priced in that solve; a later receding-horizon solve prices it once its completion becomes visible.
Note
Invalid history (non-numeric, NaN/infinity, negative values, or longer than N - 1) is ignored with a warning and falls back to empty history, meaning t0 is treated as a tariff-interval boundary. At N = 1, interval-history machinery is not used.
Passing forecast data#
There is a complete dedicated section in the Forecast section.
Specifically the Passing your own forecast data section.
WebSocket as a data source#
EMHASS can retrieve historical sensor data directly from your Home Assistant instance using a WebSocket connection. By default, this connection fetches short-term statistics at a high resolution (5-minute intervals), which is ideal for real-time optimization and Model Predictive Control (MPC). To activate this option, simply set use_websocket: true.
If you are using the EMHASS-Add-on, this connection is handled automatically via the Home Assistant Supervisor API. If you are running EMHASS via Docker standalone or Python, ensure your Home Assistant URL and a Long-Lived Access Token are correctly provided in your configuration.
Note
⚠️ Important limitation regarding long-term historical data:
When retrieving data via WebSocket, the availability of high-resolution (5-minute) data is strictly tied to your Home Assistant recorder’s purge_keep_days setting (which defaults to 10 days).
If you configure EMHASS to retrieve a history window (historic_days_to_retrieve) that is longer than your purge_keep_days, Home Assistant will silently fall back to providing long-term statistics. Because these long-term statistics are aggregated to one value per hour, they lack the necessary granularity to use them for EMHASS, for example for accurate machine learning model fitting.
Best Practices:
Use the WebSocket connection primarily for short-term history windows, MPC, and real-time optimization.
For training machine learning models over longer historical horizons (e.g., months of data), use InfluxDB as your data source, as it retains high-resolution data without aggressive purging.
InfluxDB as a data source#
A new feature allows using InfluxDB as an alternative data source to the Home Assistant recorder database. This is beneficial for users who want to treat longer data retention periods for training machine learning models or to reduce the query load on their main Home Assistant instance.
When use_influxdb: true is set, EMHASS will fetch sensor data directly from your InfluxDB instance using the provided connection parameters. The influxdb_username and influxdb_password are treated as secrets.
If you are using the Influxdb official Home Assistant Add-on, then if you have set the integration via the configuration.yaml it will look like this:
influxdb:
host: xxxxxxxx-influxdb
port: 8086
database: homeassistant
username: !secret influxdb_user
password: !secret influxdb_password
max_retries: 3
default_measurement: state
include:
domains:
- sensor
Note
Here yo need to set your own values for: xxxxxxxx-influxdb, influxdb_user and influxdb_password
Then on the EMHASS configuration you need to set:
{
"influxdb_database": "homeassistant",
"influxdb_host": "xxxxxxxx-influxdb",
"influxdb_port": 8086,
"influxdb_measurement": "W",
"influxdb_retention_policy": "autogen",
"influxdb_use_ssl": false,
"influxdb_verify_ssl": false,
}
Finally, if using the Add-on, you need to fill both “influxdb_password” and “influxdb_username” in the Add-on Configuration pane.
If using the Docker standalone or legacy installation method, then you need to set these in the secrets_emhass.yaml file.
Arithmetic expressions in the sensor list#
When using InfluxDB, the entity id you would normally give to a sensor list parameter (such as sensor_power_photovoltaics or sensor_power_load_no_var_loads) can instead be an arithmetic expression that combines several InfluxDB time series. Wrap the expression in {{ ... }} and quote each entity id:
{{'sensor.power_a' - 'sensor.power_b' * 1000}}
Each quoted entity is queried separately and the operation is applied element-wise on the values returned for the matching timestamps. The supported operators are +, -, *, /, **, %, unary +/- and numeric constants. Anything else (function calls, attribute access, comparisons, names that are not quoted entities, …) is rejected, so an expression cannot run arbitrary code. An entity referenced more than once within expressions is only queried once. This is useful when the quantity EMHASS needs is not stored as a single sensor, for example a net power that is the difference of two meters, or a unit conversion.
Note
A few caveats apply because of how InfluxDB 1.x aggregates data:
Each series is bucketed with
GROUP BY time() FILL(previous), so every entity in an expression lands on the same time grid and the arithmetic stays aligned. For the leading buckets of the requested window, EMHASS queries each entity slightly earlier (by one day) so the first values can be forward-filled from the most recent prior reading; a sensor whose previous reading is older than that still starts with a short gap, which the downstream regression absorbs.If a referenced sensor updates less often than once per day, that one-day lookback may not reach its previous reading and the expression starts with leading
NaN. If that affects your setup, add the expression tosensor_replace_zeroorsensor_linear_interpso the gap is filled before the optimization.An entity used both as a plain sensor and inside an expression is queried twice (the plain entry over the requested window, the expression entity over the padded window), so its plain column and its value inside an expression can differ at the leading buckets.
InfluxDB 1.x cannot time-weight inside
GROUP BY, so a source that updates more slowly than the optimization time step simply repeats its last value within each bucket. Keep this in mind when combining series that report at very different rates.If any entity referenced by an expression returns no data, the whole InfluxDB retrieval fails (a plain sensor that returns no data is still skipped, as before).
VictoriaMetrics as a data source#
VictoriaMetrics can be used exactly like InfluxDB (a step-by-step recipe including the migration of an existing InfluxDB 1.x history is in the cookbook): as a long-retention store that lets the machine learning forecaster train on months of history instead of the few days kept by the Home Assistant recorder. It is a good option now that the InfluxDB 1.x Home Assistant add-on is archived: VictoriaMetrics is Apache-2.0 licensed, lightweight, keeps years of high-resolution data on a small box and is available as a Home Assistant community add-on.
The nice property is that VictoriaMetrics accepts the InfluxDB line protocol for writes, so it is fed by the standard Home Assistant influxdb integration with no special configuration: point the integration at the VictoriaMetrics host and port (8428) using api_version: 1. Reads however use PromQL/MetricsQL, not InfluxQL, which is why EMHASS has a dedicated use_victoriametrics data source rather than reusing use_influxdb.
Home Assistant configuration (adapt the host name and the credentials to your setup, credentials are only needed when the VictoriaMetrics endpoint is protected by HTTP Basic authentication):
influxdb:
api_version: 1
host: a0d7b954-victoriametrics
port: 8428
database: homeassistant
username: !secret victoriametrics_username
password: !secret victoriametrics_password
include:
domains:
- sensor
The two !secret entries above are Home Assistant secrets.yaml keys used by the influxdb integration to write; they are independent from the EMHASS victoriametrics_username / victoriametrics_password parameters used to read (see below), even if it is convenient to give both the same values.
With this setup the numeric state of every sensor is stored as a metric named <unit_of_measurement>_value with the entity id (without the sensor. domain) as a label, for example:
W_value{entity_id="power_load", domain="sensor", db="homeassistant"}
EMHASS configuration only knows the entity id, not the unit of each sensor, so the metric is located by the entity_id and domain labels combined with a regular expression on the metric name (victoriametrics_metric_regex, default .+_value). The unit therefore never has to be configured. If a sensor changed unit over time (two metrics match), the metric with the most samples is used and a warning is logged; set victoriametrics_metric_regex to pin one (for example W_value).
On the EMHASS configuration set:
{
"use_victoriametrics": true,
"victoriametrics_host": "a0d7b954-victoriametrics",
"victoriametrics_port": 8428,
"victoriametrics_database": "homeassistant",
"victoriametrics_metric_regex": ".+_value",
"victoriametrics_use_ssl": false,
"victoriametrics_verify_ssl": false,
"historic_days_to_retrieve": 90
}
victoriametrics_database is optional: when set it filters on the db label (VictoriaMetrics attaches the database name given by the writer as that label unless it runs with -influxSkipDatabaseLabel). Leave it empty to not filter on it.
If your VictoriaMetrics endpoint requires HTTP Basic authentication (the community add-on does unless leave_front_door_open is set, it validates against Home Assistant users), fill in victoriametrics_username and victoriametrics_password in the Add-on Configuration pane, or in the secrets_emhass.yaml file with the Docker standalone or legacy installation method. They are treated as secrets and never written to config.json.
Each sensor is retrieved with avg_over_time(<selector>[<optimization_time_step>]) through the /api/v1/query_range API at a step equal to the optimization time step, and the result is put on the same time grid (buckets labelled by their start) as the InfluxDB and Home Assistant retrieval paths, so the forecaster and the optimizer behave the same whatever the source. The query window is the half-open interval [start_time, end_time): within it, empty buckets after the first known sample are forward-filled like InfluxDB’s FILL(previous), and the last known state is held through the final eligible bucket strictly before end_time (no bucket is ever created at or after end_time). Leading buckets before a sensor’s first known sample are left absent rather than fabricated. Long windows are fetched in chunks to stay under the -search.maxPointsPerTimeseries limit of the server, with identical terminal-fill behaviour across chunk boundaries. The {{ ... }} arithmetic expressions described above for InfluxDB are supported with VictoriaMetrics as well, and inherit this same fill behaviour.
Note
As with InfluxDB, if use_victoriametrics is set in config.json the parameters given at runtime in the POST payload (for example historic_days_to_retrieve) still take precedence, so make sure the fit/predict automations pass consistent values. When both use_influxdb and use_victoriametrics are enabled InfluxDB is used and a warning is logged.
Series scraped from the Prometheus integration#
If VictoriaMetrics already scrapes the Home Assistant Prometheus integration, EMHASS can read that history directly, without also feeding the influxdb integration. Those series name the metric after the domain, device class and unit, and keep the full entity id in an entity label:
homeassistant_sensor_power_w{entity="sensor.power_load", domain="sensor", friendly_name="Power load"}
Set victoriametrics_entity_label to entity so that the whole entity id is matched against that label (instead of the entity_id and domain labels described above), and point victoriametrics_metric_regex at the sensor metrics:
{
"use_victoriametrics": true,
"victoriametrics_host": "192.168.1.10",
"victoriametrics_port": 8428,
"victoriametrics_database": "",
"victoriametrics_metric_regex": "homeassistant_sensor_.+",
"victoriametrics_entity_label": "entity"
}
Keep the
homeassistant_sensor_prefix in the regex. The integration also exportshomeassistant_entity_available,homeassistant_last_updated_time_secondsandhomeassistant_state_change_*for every entity, with the sameentitylabel.Leave
victoriametrics_databaseempty: scraped series carry nodblabel.Units are not converted. A power sensor exported in kW (
homeassistant_sensor_power_kw) is read in kW, so rescale it with an arithmetic expression such as{{'sensor.grid_power' * 1000}}.A label that changes value over time, such as a renamed
friendly_name, starts a new series under the same metric name. EMHASS joins series that share a metric name, so the history stays continuous. If both series have samples in the bucket where the change happened, the first value is kept and a duplicate timestamps warning is logged.
Passing in secret parameters#
Secret parameters are passed differently, depending on which method you choose. Alternative options are also present for passing secrets, if you are running EMHASS separately from Home Assistant. (I.e. not via EMHASS-Add-on)
EMHASS with Docker or Python#
Running EMHASS in Docker or Python by default retrieves all secret parameters via a passed secrets_emhass.yaml file. An example template has been provided under the name secrets_emhass(example).yaml on the EMHASS repo.
To pass the the secrets file:
On Docker: (via volume mount)
Docker run ... -v ./secrets_emhass.yaml:/app/secrets_emhass.yaml ...
On Python: (optional: specify path as a argument)
emhass ... --secrets=./secrets_emhass.yaml ...
Alternative Options#
For users who are running EMHASS with methods other than EMHASS-Add-on, secret parameters can be passed with the use of environment variables. (instead of secrets_emhass.yaml)
Some environment variables include: TIME_ZONE, LAT, LON, ALT, EMHASS_URL, EMHASS_KEY
Note: As of writing, EMHASS will override ENV secret parameters if the file is present.
For more information on passing arguments and environment variables using docker, have a look at some examples from EMHASS Development section.
EMHASS-Add-on (Emhass Add-on)#
By default, the URL and KEY parameters have been set to empty/blank in the Home Assistant configuration page for EMHASS addon. This results in EMHASS calling its Local Supervisor API to gain access. This is the easiest method, as there is no user input necessary.
However, if you wish to receive/send sensor data to a different Home Assistant environment, set url and key values in the hass_url & long_lived_token hidden parameters on the Home Assistant EMHASS addon configuration page. (E.g. http://localhost:8123/hassio/addon/emhass/config)
hass_urlexample:https://192.168.1.2:8123/long_lived_tokengenerated from theLong-lived access tokenssection in your user profile settings
Secret Parameters such as: solcast_api_key, solcast_rooftop_id and solar_forecast_kwp (used by their respective weather_forecast_method parameter values), can also be set via hidden parameters in the addon configuration page.
Secret Parameters such as: time_zone, lon, lat and alt are also automatically passed in via the Home Assistants Supervisor API. (Values set in the Home Assistants config/general page)
Note: Local currency could also be obtained via the Home Assistant environment, however as of writing, this functionality has not yet been developed.