Skip to main content
Version: 2026.11

Production readiness decisions

This page records the decisions taken to get Power Insight production ready, what is deliberately parked for later, and the order they are implemented in. Engine decisions graduate from here into Engine calculation decisions as they are implemented — each with its note and its tests/engine/manual/ class — so this page is the plan, and that one is the record the tests enforce.

Status per decision: settled (decided, not yet implemented), done (implemented), parked (explicitly deferred to a later session).

Engine​

1. What a published map contains — done​

Six properties used to publish a value while a meter was unavailable, and the per-device maps dropped a device whenever it went idle. The contract:

SituationPublished
A meter is unavailable (gross power unknowable)the whole map is None
Every per-device mapcarries a key for every device of its family
Amount (W, EUR/h), device not involved0.0 — the true value
Fraction (share, ratio), denominator zero0.0 — a documented convention: a share of nothing is nothing
Price (EUR/kWh), nothing deliveredNone — a price with no energy behind it is unknown, not free

This also settles the grid-only / GridIdle open question: a grid reading exactly 0 W keeps its keys, reads 0.0 amounts and ratios, and a None price. PUBLISH_WHILE_UNAVAILABLE goes away.

Rejected: blanking fractions while idle (sensors would read unknown every night), and reporting an idle device's own configured price (it describes a kWh that was not delivered).

B. A missing grid price — done​

A missing meter blanks the whole map. A missing price blanks only the values that need it, per key: a battery charging from PV alone still has a known levelized cost. Watts, shares and ratios never move. Holding the last known price through a short dropout is a sensor-layer question, not the engine's.

A. A sink whose allowed sources are all idle — done​

It used to collapse to an all-zeros row, which moved its draw into the home base load (a "PV only" battery topping up from the grid overnight was booked as household consumption, at zero operating cost) and, for an export with no exporter running, booked exported watts as consumption. It is now relaxed like any other broken restriction: its row sums to 1, it is priced at what it actually drew, and its whole draw is its restriction_deficit. This is continuous as the allowed source fades to 0 W.

A deficit is normal operation for a battery that prefers PV but tops up from the grid, so it stays an attribute on the device's cost sensors — no repair issue. The balance law gains channel-total checks (the charging channel equals what the batteries drew, the export channel equals the grid export), which is what would have caught this.

2. Readings that overdraw — done​

Meters are sampled at different times (a grid meter every 60 s, smart plugs every 30 s) and are individually inaccurate, so the metered sinks sometimes draw more than the sources supply. The engine then routed more watts out of a source than it read.

Meet in the middle. No meter is trusted over another: every reading moves in proportion to its size, by the least that balances the books — sources up by 1 + λ, sinks down by 1 − λ, with λ = (sinks − sources) / (sinks + sources).

  • Neutral: when it is unknown which meter is stale, meeting in the middle is never fully wrong, where holding the grid or the sources fixed is either exactly right or fully wrong.
  • Proportional weighting is the only one that keeps the PV-splitting law.
  • Continuous (λ = 0 at balance), and restrictions only get easier to honour.
  • Unbiased, so timing errors average out in the running totals.

The raw-reading properties (combined_production, combined_grid_import / _export, each device's own power) keep what the meters say. gross_power, the channels, the ratios and every cost and avoided cost use the balanced figures, so every ledger balances in every snapshot. The gap is published as metering_imbalance (W): a diagnostic sensor, created with the other debug sensors (the integration's opt-in for diagnostics), plus a repair issue when it persists — raised above 10 % of gross power averaged over 15 minutes, cleared below 5 %. A sustained imbalance means an inverted sign or a meter nested inside another metered circuit. Only overdraw is corrected; an underdraw is indistinguishable from real unmetered consumption and stays in the base load.

Possible later refinement: weight each reading by its age (last_reported), which would put most of the correction on the stale meter. Deferred: it makes the engine depend on timestamps, and sensors that only report on change look stale while being accurate. Revisit once metering_imbalance shows how large real imbalances are.

C. Savings are measured against a home without the device — done​

Self-consumption is valued at the import price even while the house exports. That is not a simplification: a saving answers "what would this have cost in a home without this device?", and without the PV system that kWh would have been imported. The feed-in rate answers a different question, and exported kWh are already credited through the export compensation. The "known simplification" wording in the decision log becomes a decision.

D. The cost of stored battery energy — parked​

Booking at charge time already makes savings exact over a full cycle; only a "what did my stored energy cost" sensor is missing. Deferred past 1.0.

E. CO₂ — kept as is​

The config flow keeps collecting the CO₂ entities and footprints; the engine still computes no CO₂ result. When CO₂ sensors are built, they follow the correction rules for cost (decision "CO₂ is not corrected until something publishes it").

Already settled elsewhere​

The open questions in the allocation write-up are historical: 1–5 were answered by the max-flow solver, 6 (hard restrictions) and 7 (grid first) are decisions in the log.

3. Correction factors — done​

The engine was already consistent; what was missing was the promise and its tests. Now:

  • The harness's battery takes a correction_factor, and TestBatteryFactorScalesItsOwnLcosOnly pins that a battery's factor prices its discharge while its charging follows its sources' factors.
  • The eight *_corrected properties are catalogued, each with an identity, and so frozen. Two laws hold over the random homes, which now carry factors: a correction is a restated lifetime cost and moves nothing else, and every *_components row adds up before and after correction.
  • The sensor layer: a removed device's share of another device's total keeps its last factor ("a removed device's correction is final"); a total with no breakdown (restored from before it existed) reads at face value; the set_value service that could also seed one was later retired.
  • CO₂ is deliberately uncorrected until something publishes it ("CO₂ is not corrected until something publishes it").
  • A correction restates the device's whole history, and replaced hardware is added as a new device — there is no "replace hardware" action ("a correction restates the device's whole history"). A reconfigure without lifetime values keeps the stored base, so the correction is never re-based: the form already refills emptied fields and rejects 0, and calculate_fields now keeps the stored values if one ever got through.

Home Assistant layer​

#DecisionStatus
H1Running totals integrate up to the moment a reading becomes unavailable, then pause until it returns. No staleness timeout — HA's availability is trusted, and sensors that only report on change are legitimately silent. At startup a total starts as soon as the engine has all its readings, not at the first source event. Idle devices no longer pause a total (item 1).done
H2Left-Riemann integration: every engine input is a held value between events, so every rate is a step function and left-Riemann is exact; trapezoidal smears each step backwards.done
H3Price units: normalise EUR/kWh, ct/kWh, EUR/MWh and EUR/Wh; reject any other unit in the config flow; at runtime an unknown unit makes the price None (decision B). A price in another currency than Home Assistant's raises a repair issue instead of being converted.done
H4A power entity may belong to one device only: the config flow rejects a duplicate, and existing entries that have one get a repair issue.done
H5The combined levelized totals keep summing the per-device totals plus the retired-adapter ledger, but read unavailable while any enabled per-device total is unknown or unavailable (no more false drops in the statistics). A disabled per-device total is skipped, and that is documented on the sensor.done
H6Replace the custom bus events with a dispatcher signal (the recorder stores every custom bus event, including one per state_reported) and delete the event.py fork. One signal per config entry and source entity, carrying the reading's timestamp; sensors connect once per source. No throttle is needed.done
H7Share sensors key their unique id on the source device's subentry id, not its display name. Must land before 1.0. Existing registry entries are re-keyed in place by the 1.4 migration, so their history carries over.done
H8Migration refuses a config entry from a newer major version (downgrade). Version 1.4 (which carries H7's re-keying) is the 1.0 baseline; every storage change after it needs a migration.done
H9Repair issues: no_grid_configured gets a per-entry id; a consumer's power_from is checked like a battery's charge_from; an issue is deleted once it no longer applies (on reconfigure, or with its device). Since Home Assistant 2026.11 every issue belongs to its config entry (config_entry_id, deleted with the entry), and setup runs its checks inside ir.async_refresh_issues, so an issue still valid is kept — an ignored one stays ignored — and one setup cannot judge is kept with ir.async_keep_issue.done
H10exports_power and export_compensation become reconfigurable and apply from then on (a feed-in rate is a time-varying price, not a lifetime average, so it never rewrites history). The currency-blind 0.08 default is dropped: a value is required when exporting. A feed-in entity is post-1.0.done
H11iot_class: calculated; integration_type stays hub; version by month (2026.x.x); entity names by translation_key; the diagnostic sensors (metering_imbalance, available power) get entity_category: diagnostic and are opt-in through the debug-sensors option; diagnostics hold only entity ids and titles, so nothing is redacted.done
H12Stale docs: the efficiency note in concepts.md, CLAUDE.md's description of uids (they are subentry ids) and of the integration method, the tests README's known gaps, and the allocation write-up's open questions.done

Implementation order​

  1. Engine item 1 (the map contract), then decision B — item 2 builds on both.
  2. Engine decision A.
  3. Engine item 2 (meet in the middle) and metering_imbalance.
  4. Decision C and the documentation tidy-up.
  5. The Home Assistant layer: H7 and H8 first (they break history if they land after a release), then H1/H2, H3, H4, H5, H9, H10, H11, H12, and the metering_imbalance sensor and repair issue.

Parked for later sessions: H6 (dispatcher signal), decision D, and the age-weighted refinement of item 2.