Skip to content

microgrid.cli.history

What a plant's meters recorded, read for the commands that score against them: the battery's lots (ledger), the plant model against its meters (check-plant), the valuations' scores (replay) and the battery's fit (fit-battery).

The record is read through History, a port a dataset answers (DatasetHistory) and any recorder a client has (a service's database). Bindings say which of its series is which: the plant's meters, the SOC, the house meter, the heartbeat that proves the telemetry path alive, and the recorded prices.

The meters are integrated by the library's ledger (microgrid.engine.ledger: PV, grid, battery and a DC-bus load, allocated at every change, only while the path was alive: within the heartbeat's limit where the bindings name one, else wherever each series is known), the SOC read at each slot boundary, and the prices each slot was charged at as the time-weighted mean of the recorded price in force over it (series.means): any price resolution, on any slot, and no price read before it took effect. A recorder that writes a change late (a sensor updating seconds after the hour) is the client's to correct at its edge, as for any series. A PV lot's basis is the export price when it was charged (what the site gave up to store it), a grid lot's the import price (docs/methodology/lots.md). Every function takes the slot it works on (EngineConfig.slot; SLOT by default).

DAY module-attribute

DAY: Final = timedelta(days=1)

How far back a value is carried into a window: a day (a sensor that holds still, PV at night, writes nothing).

WEEK module-attribute

WEEK: Final = timedelta(days=7)

Read raw meters a week at a time: a busy sensor writes ~15k rows a day.

TELEMETRY module-attribute

TELEMETRY: Final = timedelta(minutes=3)

How long a heartbeat may be silent before the readings it vouches for are unknown, where the configuration says nothing (a dataset's limits, a command's --stale-after). Three minutes suits a heartbeat that reports every few seconds: it was measured on one home's plant, whose heartbeat's longest silence in a week was 157 s. A recorder with another cadence sets its own.

DC_STALE_AFTER module-attribute

DC_STALE_AFTER: Final = timedelta(minutes=5)

How long a DC-bus load's reading other than 0 may be silent before it is stale (the 0 that ended its draw was lost), where the configuration says nothing (a dataset's limits, a command's --dc-stale-after). Five minutes suits a meter that writes every few seconds while it draws: measured on one home's DC charger, which wrote every 5 s (median) and was silent at most 103 s during a session. A meter with another cadence sets its own.

Limits dataclass

Limits(
    telemetry: timedelta = TELEMETRY,
    dc_stale_after: timedelta = DC_STALE_AFTER,
)

How a record's silences are judged: the heartbeat's (TELEMETRY by default) and a DC-bus load's reading other than 0 (DC_STALE_AFTER).

telemetry class-attribute instance-attribute

telemetry: timedelta = TELEMETRY

dc_stale_after class-attribute instance-attribute

dc_stale_after: timedelta = DC_STALE_AFTER

LotsError

Bases: RuntimeError

The record cannot be read as the commands need it: no prices, no SOC.

History

Bases: Protocol

States as recorded, each as a series over [start, end) from the value in force at start (the last within carry_in before it), known until end. chunk: read so much at a time (a recorder's concern; a dataset ignores it).

power_series async

power_series(
    entity_id: str,
    start: datetime,
    end: datetime,
    *,
    carry_in: timedelta,
    chunk: timedelta = WEEK,
) -> Series[float]

Power in kW, converted from the entity's unit.

numeric_series async

numeric_series(
    entity_id: str,
    start: datetime,
    end: datetime,
    *,
    carry_in: timedelta,
    chunk: timedelta = WEEK,
) -> Series[float]

Any numeric entity's values, the unit unchecked.

sample_times async

sample_times(
    entity_id: str,
    start: datetime,
    end: datetime,
    *,
    carry_in: timedelta,
    chunk: timedelta = WEEK,
) -> tuple[datetime, ...]

When the entity's rows arrived (a liveness sensor's).

DcLoadMeter dataclass

DcLoadMeter(
    entity: str, carry_in: timedelta, stale_after: timedelta
)

A DC-bus load's meter, and its own rules: a reader that writes only on change has its last reading carried in from carry_in back, and a reading other than 0 silent for stale_after is stale.

entity instance-attribute

entity: str

carry_in instance-attribute

carry_in: timedelta

stale_after instance-attribute

stale_after: timedelta

Bindings dataclass

Bindings(
    plant: Plant,
    pv: str,
    grid: str,
    battery: str,
    soc: str,
    soc_unit: str,
    house: str,
    heartbeat: str | None,
    telemetry: timedelta,
    import_price: str,
    export_price: str,
    inverter: str | None = None,
    dc_load: DcLoadMeter | None = None,
)

Which recorded series is which, for a plant (its meters: PV in total, the grid, the battery, and a DC-bus load where it has one).

plant instance-attribute

plant: Plant

pv instance-attribute

pv: str

grid instance-attribute

grid: str

battery instance-attribute

battery: str

soc instance-attribute

soc: str

soc_unit instance-attribute

soc_unit: str

The SOC's unit (%; empty: a fraction).

house instance-attribute

house: str

The house meter: the load the plant served, as it meters it.

heartbeat instance-attribute

heartbeat: str | None

A series whose samples prove the telemetry path alive, and how long one does (TELEMETRY). None: no heartbeat; each series is then trusted as far as it is known (Series.known_until), its gaps as samples with no value.

telemetry instance-attribute

telemetry: timedelta

import_price instance-attribute

import_price: str

The import and export prices in force, as recorded.

export_price instance-attribute

export_price: str

inverter class-attribute instance-attribute

inverter: str | None = None

The battery inverter's AC power, > 0 out (None: not metered).

dc_load class-attribute instance-attribute

dc_load: DcLoadMeter | None = None

timezone property

timezone: ZoneInfo

Recorded dataclass

Recorded(
    starts: tuple[datetime, ...],
    flows: tuple[Flows | None, ...],
    p_import: tuple[float, ...],
    p_export: tuple[float, ...],
    stored_after: tuple[float | None, ...],
    stored_before: float | None,
    step: timedelta = SLOT,
)

What was recorded over a window, slot by slot, ready for the ledger.

starts instance-attribute

starts: tuple[datetime, ...]

flows instance-attribute

flows: tuple[Flows | None, ...]

p_import instance-attribute

p_import: tuple[float, ...]

p_export instance-attribute

p_export: tuple[float, ...]

stored_after instance-attribute

stored_after: tuple[float | None, ...]

Metered stored energy at each slot's end, kWh (None: unknown).

stored_before instance-attribute

stored_before: float | None

Metered stored energy at the window's start, kWh.

step class-attribute instance-attribute

step: timedelta = SLOT

The slot the window is read on.

hours property

hours: float

A slot's length in hours.

LedgerRun dataclass

LedgerRun(
    starts: tuple[datetime, ...],
    slots: tuple[LedgerSlot, ...],
    p_import: tuple[float, ...],
    p_export: tuple[float, ...],
)

The ledger over a window, with the prices each slot was valued at.

starts instance-attribute

starts: tuple[datetime, ...]

slots instance-attribute

slots: tuple[LedgerSlot, ...]

p_import instance-attribute

p_import: tuple[float, ...]

p_export instance-attribute

p_export: tuple[float, ...]

read_recorded async

read_recorded(
    history: History,
    bindings: Bindings,
    start: datetime,
    end: datetime,
    *,
    step: timedelta = SLOT,
) -> Recorded

What was recorded over [start, end), slot by slot of step.

A slot's prices are the time-weighted means of the recorded prices in force over it; a slot without both moves nothing (its flows None). Raises LotsError when no slot of the window has a recorded price.

step_slots

step_slots(
    ledger: Ledger,
    recorded: Recorded,
    plant: Plant | None = None,
) -> list[LedgerSlot]

recorded's slots, each stepped through ledger: with plant, each slot also as its model's (Plant.metered_slot), so a ledger with the plant's storage books each charge by its path (Ledger).

run_ledger async

run_ledger(
    history: History,
    bindings: Bindings,
    start: datetime,
    end: datetime,
    *,
    step: timedelta = SLOT,
) -> LedgerRun

The ledger over [start, end) on slots of step; what the battery held at start is unknown energy.

stored_costs

stored_costs(
    recorded: Recorded,
    plant: Plant,
    lots: Series[Lots] | None = None,
) -> list[float | None]

Per slot of recorded, the running average cost of what the battery held at its start, per stored kWh (Ledger.pool): what a battery kWh realised under the stored-cost rule. From lots (the ledger the valuations read, so both sides start alike) where they are in force; else the ledger over recorded, all it held at the window's start of unknown origin, at the first slot's import, as if bought (R-7).

read_recording async

read_recording(
    history: History,
    bindings: Bindings,
    pinned: Plant,
    first: date,
    last: date,
    *,
    step: timedelta = SLOT,
) -> lgt.Recording

The local days first to last's meters for the ground truth, on slots of step, split through the pinned conversion without the DC-bus loads (lgt.meters_plant), and the DC-bus load's meter where the plant has one.