Data sources¶
A sources/1 file says where each input comes from, for microgrid now, microgrid
sources and microgrid fetch (the commands). Each forecast input
(prices, the PV forecast, the load forecast) comes from one of several
providers: any entity's attribute, or a preset for a named
tool. Measured values ([measured]) are read from Home Assistant only;
without it, they come from a dataset you build. The readers
need the sources extra (Install).
Get prices from your own sources builds a file step by step.
schema = "sources/1"
plant = "plant.toml"
[home_assistant]
url = "http://homeassistant.local:8123"
[prices]
from = "tariffkit"
[pv_forecast]
from = "entity"
entity = "sensor.my_pv_forecast"
attribute = "forecast"
period_minutes = 30
keys = { value = "watts" }
unit = "W"
[load_forecast]
from = "emhass"
[measured]
from = "home_assistant"
soc = "sensor.battery_soc"
dispatch_as_modelled = true
The file¶
| Key | Required | Meaning |
|---|---|---|
schema |
yes | "sources/1" |
plant |
yes | the plant's plant/2 file |
engine |
no | an engine/2 file; without one, the library's defaults (an engine.toml beside the file is not read unless named) |
[home_assistant] |
when a section reads Home Assistant | the connection |
[prices], [pv_forecast], [load_forecast] |
no | each input's provider |
[measured] |
no | what the plant measures |
- Paths are relative to the file.
- An input with no section is not read. A valuation that needs it names it as a cause and falls back; nothing is guessed in its place.
- No entity has a default, because each site names its own. The exceptions are tools that publish under a fixed name (EMHASS, Predbat); a section may rename those too.
- Every problem is named at once, with its path (
prices.from: must be one of tariffkit, entity). A key the schema does not have is an error.
[home_assistant]¶
| Key | Default | Meaning |
|---|---|---|
url |
required | the instance's base URL: https, or http to a host on the local network (a name without a dot, .local, .lan, .home.arpa, .internal, .localdomain; or a private, loopback, link-local or carrier-grade NAT address, such as Tailscale's) |
token_env |
"MICROGRID_HA_TOKEN" |
the environment variable holding a long-lived access token: HA_TOKEN or a name ending in _HA_TOKEN |
The token is never in the file. It is read from the variable token_env names, so
the file can be shared or committed. Because the token is sent to url, a file cannot
send another variable, nor send the token in cleartext beyond the local network: a
shared file pointing elsewhere is refused. Use a token of a user of its own, not an
administrator's: the library only reads states and calls TariffKit's and EMHASS's
services (TariffKit's config-entry discovery is the one call that needs an
administrator; name config_entry to avoid it). The section is required when any section reads
Home Assistant: entity, tariffkit, solcast, emhass, predbat, or [measured].
Units¶
Power is W, kW or MW. A price unit is a currency, as a code or a symbol, before /Wh,
/kWh or /MWh (EUR/MWh, USD/kWh, ยข/kWh); per-Wh and per-MWh prices become per
kWh, and the currency is never converted (Currency).
Any entity: from = "entity"¶
Any input can come from an entity whose attribute holds a list of rows, each with a start time, an end time or a fixed length, and its values.
| Key | Default | Meaning |
|---|---|---|
entity |
required | the entity |
attribute |
required | the attribute holding the rows |
start_key |
"start" |
each row's start time |
end_key |
"end" |
each row's end time; leave it out and set period_minutes for rows of a fixed length |
period_minutes |
none | each row's length, where rows have no end time |
keys |
required | which of the row's keys holds each value: import and export for prices; value, or p10, p50 and p90, for PV; value for a load |
unit |
the entity's unit_of_measurement |
the values' unit: power for PV and loads; a price unit for prices, where none means per kWh |
Times are ISO 8601 with an offset. The issue time is the state's last_updated. A row
that cannot be read is an error, not skipped, since nothing is known about the provider's
faults.
A provider that does not publish rows like these cannot be read as an entity, for example one whose entities hold only daily energy totals, or only the current power. Check what your integration's entity holds (its attributes, in Home Assistant's developer tools) before configuring it.
Presets for named tools¶
These read a named tool's published shape. Each is an option, never a default.
Prices: from = "tariffkit"¶
TariffKit computes a utility tariff's prices; its Home Assistant integration's
get_rates service returns all-in import and export prices per kWh, by the hour.
| Key | Default | Meaning |
|---|---|---|
config_entry |
the one loaded | the integration's config entry, where more than one is loaded |
The reader asks for the horizon plus one hour from the top of the hour under way, so a
slot that starts mid-hour is still priced at the far end; the horizon may be at most
167 hours. The answer is one forecast issue, stamped when TariffKit answered
(generated_at). A gap between its hours is an error. TariffKit prices in US dollars,
and a dataset saved from it says USD/kWh.
PV: from = "solcast"¶
The Solcast integration publishes one entity per forecast day, each with a
detailedForecast attribute of half-hourly rows: the mean kW at P50 (pv_estimate),
P10 and P90.
| Key | Default | Meaning |
|---|---|---|
entities |
required | the daily forecast entities, today's first; today, tomorrow and day 3 cover a 48-hour horizon from any time |
last_polled |
required | the entity holding the integration's last successful poll |
The forecast's age is the last poll, not the entities' update times, which move at every local midnight and restart. An entity that cannot be read is skipped and named; two that disagree about a half hour are an error; at a gap the forecast stops.
Loads: from = "emhass"¶
EMHASS publishes its load forecast as an entity whose attribute lists rows such as
{"date": "2026-10-12T22:00:00-07:00", "p_load_forecast": "1511.82"}: the mean power
over its optimisation step from date.
| Key | Default | Meaning |
|---|---|---|
entity |
"sensor.p_load_forecast" |
the entity |
attribute |
"forecasts" |
the attribute holding the rows |
value_key |
"p_load_forecast" |
each row's value |
period_minutes |
30 |
EMHASS's optimisation step: each row's length |
The unit is the entity's unit_of_measurement. The issue time is the state's
last_updated. A row that cannot be read is skipped and logged; two rows for one time
that disagree, rows that overlap, or no usable row are an error.
Loads: from = "predbat"¶
Predbat publishes today's predicted load as cumulative energy from local midnight, in kWh, by time.
| Key | Default | Meaning |
|---|---|---|
entity |
"predbat.load_energy_predicted" |
the entity |
attribute |
"results" |
the attribute mapping times to cumulative kWh |
step_minutes |
5 |
Predbat's step: each value includes the step starting at its time |
The load over each step is the energy it adds, as a mean power. Predbat publishes
today only, so the valuation's horizon ends at local midnight: a shorter valuation,
not a fallback, with no cause named and horizon_cut false. The first time's own step
is not read, because the energy before it is unknown; Predbat writes from local
midnight, so that is the night's first 5 minutes and the slot under way is covered. Only
a forecast that starts mid-day loses its first 5 minutes. The issue time is the state's
last_updated.
A load forecast must cover the slot under way¶
now reads one issue, received now. Some load forecasters publish from the next step
(some EMHASS setups publish rows from the next half hour), so that issue alone leaves the
slot under way uncovered, and the valuation falls back
with load_forecast named. The engine would take such a slot from the previous issue
(the rule for loads),
but one read has no previous issue. A client that runs continuously and keeps every
issue it receives does (Run it continuously).
[measured]¶
What the plant measures, read from Home Assistant.
| Key | Default | Read by | Meaning |
|---|---|---|---|
from |
required | "home_assistant" |
|
soc |
none | now, fetch |
the battery's SOC |
soc_unit |
"%" |
now, fetch |
the SOC's unit where its entity states none; empty: a fraction |
dispatch |
none | now, fetch |
an entity whose state is the plant's mode |
modelled_modes |
required with dispatch |
now, fetch |
the modes in which the plant runs the dispatch its model describes |
dispatch_as_modelled |
none | now |
true or false for always, in place of dispatch (not both) |
[measured.meters] |
none | now (the lots), fetch |
each of the plant's meters, by its plant/2 name, to an entity |
house |
none | now (the lots), fetch |
the house load |
heartbeat |
the house load's entity | now (the lots), fetch |
an entity that reports every few seconds (heartbeat) |
import_price, export_price |
none | now (the lots), fetch |
the recorded prices, in a price unit |
inverter |
none | fetch |
the battery inverter's AC power, positive out |
The mode entity's states are compared as text with modelled_modes: the plant runs as
modelled while its state is one of them (dispatch as
modelled).
The commands¶
| Command | Reads | Writes |
|---|---|---|
microgrid sources --sources FILE |
every configured input, now | one line per input: yes and its provider, or no and why; exits 1 if any is not read |
microgrid now --sources FILE [--slots N] [--save DIR] [--lots-run-in D] |
every configured input, now; for a plant with a battery, [measured]'s history over the two days before now (--lots-run-in), for the battery's lots |
a valuation at the current time, as value prints one; with --save, the inputs read as a dataset, with the plant's and engine's files |
microgrid fetch --sources FILE --from T --to T --out DIR |
[measured]'s history over the span |
a dataset of what was measured, for check-plant, ledger and fit-battery |
now and sources read each forecast once, as one issue received now, and each
measured value as the value in force, from when it changed. An input that cannot be read
is reported on standard error and left out, and the valuation names its cause.
For a plant with a battery, now also runs the battery's
ledger over what [measured]'s meters, SOC and recorded prices
recorded before now, as fetch reads them, and hands the engine the lots it ends with:
a battery kWh is priced at what the energy it holds cost. Where it cannot (no
[measured] meters or recorded prices, no heartbeat or house meter, nothing recorded),
standard error says why (not read: battery_lots: ...) and the valuation names
battery_lots, everything held priced as bought at the import.
A dataset written by now --save re-runs with microgrid value --data DIR --at T, where
T is the time of the read: the prices entry's received_at. At any earlier moment
nothing had been received, so the valuation is unpriced.
fetch reads Home Assistant's recorded history (its changes; the recorder keeps ten days
by default), in the unit each entity states now. It needs a heartbeat or the house load's
entity: a meter that writes only changes (PV at 0 all night) would otherwise mark the
record silent. Times without an offset are the plant's local time; --from is included,
--to is not. For check-plant, whose --to is an included day, fetch from the first
day's local midnight to the midnight after the last:
uv run microgrid fetch --sources sources.toml --from 2026-10-01T00:00 --to 2026-10-08T00:00 --out record
uv run microgrid check-plant --data record --from 2026-10-01 --to 2026-10-07
check-plant's hourly 48-hour runs stop at the data's end, so the last day's runs are
shorter.
Not supported yet¶
- Measured values from anything but Home Assistant.
[measured]andfetchread Home Assistant only. Without it, write the measured series into a dataset yourself. - A recorder other than Home Assistant's for
fetch, such as InfluxDB. - Forecast history.
fetchwrites what was measured, not forecasts: Home Assistant does not keep what a forecaster published in a form that says when it was known. A dataset forvalue,liveorreplayneeds its forecasts recorded as they were received.now --savewrites the inputs of one moment, and a second save to the same directory replaces the first; keeping the issues over time is a continuous client's job (Run it continuously). - PV forecasts per array. A plant with arrays of both couplings takes one forecast
per array, which
sources/1does not read yet;nowsays so. Use a dataset withpv_forecast.<array>entries.