Files
MeterVault/docs/RELEASE_NOTES.md
T
Florian Schmidt a08e9f781f
ci / build-test (push) Successful in 2m41s
Analysis: read a rarely-read meter as coarse, not absent; newest rows first
Three things a reported Heizoel page got wrong at once. Its tank is dipped
a few times a year and its burner read every few months, which is exactly
the shape the coverage rules had not been walked through.

"No data" for data that exists. A tank books nothing until the next
dipstick closes the interval, so the stretch after the last dipstick is
covered by no run at all, and a bucket no run covers was reported missing.
The burner, whose run reaches into the window, said "only coarser data" --
the honest answer -- so one card claimed there was nothing while the
coverage panel beside it listed years of data. A bucket that no run covers,
no gap overlaps and no opening balance explains now reports the meter's
resolution when its preceding coverage is within one interval of its own
class: it is not silent, it is read rarely. A meter that does book its own
buckets and stops -- a dead hourly source, a sheet asked about a later
month -- still reads missing.

Auto answering twelve months with one bar. Coarse only means "longer than
a month", so a dipstick taken each autumn straddles a New Year as surely
as a month start: coarsening the chart to years bought nothing and cost
every point. The planning resolution now caps coarse at month when a run
crosses a local year edge, and a series that cannot resolve the natural
size no longer coarsens the whole chart -- it is drawn at that size with
its buckets marked, which the chart and table already explain.

A page contradicting itself. The comparison line above the ranking was fed
the leading measure's matched coverage but worded as if it spoke for the
page, directly above a burner row that did compare. It now names the figure
it is about.

Alongside: the "largest changes" ranking no longer drops a meter whose
change is not comparable. It ranks what can be ranked, then lists the rest
with their values and the reason -- the tank had been vanishing from its
own energy type. And every dated table now reads newest first, as lists
are read; charts stay chronological left to right, and the CSV export
stays ascending for spreadsheets.

A-41 to A-43 in the note record the three rules.
2026-09-20 12:44:32 +02:00

20 KiB
Raw Blame History

Release notes

User-visible changes per release. Earlier releases are described in the git history and in the README's upgrade sections. Decision ids (D-nn, A-nn) refer to ANALYSIS_IMPLEMENTATION_NOTE.md.

0.4.0 — Dashboard, navigation and historical analysis (unreleased)

This release rebuilds how MeterVault analyses and prices data, and the pages that show it. Every page now works on the same selected period and the same numbers. A virtual (calculated) meter can be analysed like a physical one. A true zero, missing data, data that exists only per month, and a missing price are always told apart.

Some figures change on purpose. Most visibly, an energy type's bill now counts its grid meter, not every meter that exists. Changed figures lists every deliberate change. The seeded demo's yearly costs now match the spreadsheet: 2025 comes to 7,907.65 € against the sheet's 7,907.64 €. The seeded "this year" cost for 2026 was 4,402.19 € in 0.3.0, which priced Haus + Netz + Auto and water at 5 €/m³. It is now 2,940.19 €, the sheet's figure.

Upgrading

Back up the database (pg_dump) first.

  • The first start rebuilds all analysis data (normalization revision 3):
    • For every meter it rebuilds consumption and the new day and month rollups plus their coverage. It records the state of each meter.
    • This runs before the web server listens. The app is unreachable meanwhile, and Compose may report the container unhealthy. Let it finish.
    • The time grows with the number of raw readings. One meter took about 0.05 s with monthly readings, ~0.5 s with ten years of daily readings (3,700 readings), and ~1.2 s with a year of hourly readings (9,300 readings).
    • A synthetic 1,000-meter × 10-year instance (1.34 M readings) took 4.7 minutes (279 s) on an idle Ryzen 9 9950X3D — about 3.6 meters a second. It writes rollups and coverage on top of consumption, so it does more per meter than the 0.3.0 rebuild did.
    • Progress is logged every 100 meters. A meter that fails is logged, kept pending and retried at the next start. A meter whose stored consumption is older than its oldest remaining reading is skipped and logged, so its history is not truncated.
    • Until a meter is rebuilt, its pages say "analysis being prepared", never "no data".
  • The migration records each meter's last reading time in the analysis state table and fills it in for the meters you already have, in one pass over the readings. It is what the pages show as "last activity" and what decides whether a live source is late, so no page has to search the raw readings for it any more.
  • Virtual meters without a formula (such as a seeded Summe Solar from an earlier version) are converted at startup. When their incoming links name meters of one unit and kind, the implied sum is stored as an explicit formula (D-28). The log lists the converted meters, and the meters that still need configuration because their links are ambiguous, in mixed units, or loop. The conversion runs once; a rerun changes nothing. From then on, links are topology only and never change a calculation.
  • The migration drops the unused continuous aggregates and their hourly refresh jobs. It also deletes consumption rows stored for virtual meters, which nothing read (D-17).
  • Check the attention items on the Overview after the first start. They name missing prices with a direct "Add tariff" link, invalid calculations, stale live sources, rows dated after now, and meters that may be counted twice.
  • Rolling back to 0.3.0 works. The old version ignores the new tables and never read the dropped aggregates. Consumption stays as 0.4.0 booked it until each meter next ingests a reading. Stored virtual formulas remain, and 0.3.0 goes back to summing links.

What's new

Navigation.

  • The sidebar has a fixed structure: Overview, Analysis, Meters, an Energy types group, Specialized views (Solar, Tanks & consumables; always listed, with a setup hint when not configured), Data import, and Configuration.
  • "Configuration → Energy types" edits the definitions; the Energy types group is for analysis.
  • Expanded groups are remembered, and the current page's group always opens. If the energy types cannot be loaded, the menu shows an error with Retry instead of silently dropping them.
  • Breadcrumbs (Overview → energy type → meter) keep the selected period, and so does Back.
  • "Find a meter" opens the meter's analysis with the current period and still offers quick entry. It is a text button on desktop and an icon on phones.

One period everywhere.

  • Every analysis page has the same toolbar: this month to date, last month, year to date, previous year, last 12 or 24 months, all history, or custom dates with one Apply.
  • It also offers a bucket size (automatic, day, week, month, year) and a comparison: previous period, same period last year (the default), or any calendar year.
  • The effective dates are shown next to the choice, with the time zone. The selection lives in the address, so reload, Back and shared links reproduce the page.
  • Rapid clicks can no longer leave one page showing another selection's data.

Overview.

  • It shows one selected period (default month to date):
    • the cost, split into metered use, standing charges, manual costs and feed-in credit;
    • a card per energy type with its quantities in their own units, its cost and billing basis, the change and freshness;
    • a history chart with a table view;
    • "What changed", by category or by meter;
    • the cost composition;
    • attention items, each with one targeted action.
  • Changes are compared only over the part both periods cover, and the page states both date ranges.
  • When the period has no data, the page names the dates that do have data and offers "Go to latest data". It never silently switches to an older month.

Analysis page (/trends, formerly the cost trend).

  • Explore everything, one energy type, a cost category, one meter, or up to six meters side by side, by quantity or by cost.
  • Compare calendar years with an overlay and a comparison table. Click a bar or a row to drill into a finer period.
  • Export exactly what is shown as CSV.

Energy type pages are titled with the type's own name and have four tabs:

  • Overview: measures such as total use, grid import, generation and runtime, each in its own unit and never added across units; the cost with its billing basis; coverage; the largest changes.
  • History: the total, or up to six individual meters with an explanation of how each one counts.
  • Flow: the Sankey, now using the same values as every other page. Calculated and estimated connections are marked. It comes with a table version and Manage connections.
  • Meters: each meter's value for the period and its data quality.

Meter page.

  • The tabs sit directly under the header: Analysis, Readings, Normalized data, Events, Tariffs, Sources. A calculated meter has Calculation instead of Sources.
  • The Analysis tab shows:
    • the period total with its unit and status, and the cost with its rule, or the reason it has none;
    • the change against the comparison period;
    • a labelled projection, where there is enough data for one;
    • a full chart with the previous-year overlay, and a table;
    • a "Data quality and coverage" section;
    • events and tariff changes in the range.
  • The record tabs page through the whole history (100 rows at a time, filtered by the selected dates), no longer just the latest 200. Rows dated after now are marked.
  • Existing links such as ?tab=consumption, ?tab=readings&action=reading and ?tab=events&action=swap still open the intended tab and dialog once.

Virtual (calculated) meters.

  • Create them with Sum, Difference or a Formula over other meters, picked by name. A live preview of the selected period shows every source's values and flags incomplete months.
  • The result kind (consumption, generation, net or indicator), unit and cost rule are stored with the formula.
  • A virtual meter gets the same analysis as a physical one, plus "Source meters" showing each source's contribution.
  • The rules:
    • A missing source month makes the result "no data" for that month, never a silent zero.
    • An observed zero is a real zero.
    • A division by zero or a loop is reported, and names the meters involved.
    • Differences stay negative.
  • New calculated meters are analysis only: they never add to a type's totals or the bill. The "Always count" option lets one replace its sources instead.

Solar and Tanks & consumables use the same toolbar, cards and charts.

  • Solar works out self-consumption, feed-in, site use, savings and autarky from the meters' roles. For a missing role it shows a setup card with candidate meters instead of raw role tags.
  • Tanks keep "Last dipstick (date)" apart from "Estimated now". A past period shows the contents at its end, not today's. The forecast is a labelled projection, hidden when the dipstick is older than 60 days.

Tariffs and configuration.

  • An "Add tariff" link from a missing price opens the tariff editor once, prefilled with the scope, component and first uncovered month.
  • The unit is checked against what it prices: a wrong unit or currency blocks the save.
  • A new tariff needs a value; a typed 0 is a deliberate free period.
  • The editor notes that Bonus, Discount and Tax are stored but not applied yet.
  • Meter roles have friendly names and one-line meanings. A role is unique per energy type, and saving it names the meter it moves from.
  • The Settings page shows the analysis data state and that raw retention is not enforced.

Also:

  • CSV export of any analysis view (/export/analysis.csv): statuses, provenance, costs and comparison values; unknown values are empty cells, never 0.
  • The light/dark choice persists across reloads and language switches, and charts follow it immediately.
  • Visible keyboard focus, and tables for every chart.
  • MudBlazor's own labels are in German too.
  • Pages work at phone width.
  • Amounts use the configured currency (MeterVault__Currency) instead of a hard-coded €.
  • On a page that shows several figures, the line about the compared dates names the figure it is about, so it can no longer say "not comparable" above a table that compares another meter month by month (A-41).
  • Every dated table reads newest first, with the total above the rows it sums: the meter's Analysis tab, an energy type's History, the Analysis page, Solar, Tanks & consumables, the tariff lists and a virtual meter's preview. The meter page's events and price changes are one dated list instead of two. Charts and the CSV export stay chronological, because that is how they are read (A-42).
  • "Largest changes by meter" no longer leaves out a meter it cannot rank: one that has data but no comparable change — a tank dipped a few times a year — is listed under "Other meters" with both its values and the reason there is no change. A meter with nothing on either side stays out, and whatever the caps leave over is counted out loud (A-43).
  • Deleting a meter or an energy type also deletes the tariffs scoped to it.
  • JSON export/import now carries meter connections, re-links virtual formulas to the new meter ids, and no longer restores the tariffs of deleted meters onto other meters.

Changed figures

Every change below is deliberate. The golden spreadsheet reconciliation (consumption of all four sheets, Netz Einsparung) is unchanged. The seeded yearly bill matches the sheet's Jahreskosten within ±0.02 € for 2022, 2025 and 2026. It differs by 3.78 € (2023) and 0.46 € (2024) only because the sheet multiplies by unrounded prices.

Area 0.3.0 0.4.0
What an energy type's bill counts Every meter's cost was summed: Haus + Netz + Auto for the seeded Strom The type's grid import meter when it has one, otherwise its household use. Submeters are breakdowns; generation is never billed. Seeded Strom is Zähler Netz × price, like the sheet (2025: 4,742.64 €) (D-22, D-34)
Feed-in credit Credited on all generation Only on a meter with the grid-export role, at the feed-in price (D-34)
A subsection with its own meter price Added on top Billed at its own price and taken out of the meter above it; quantities unchanged (D-35, A-19)
Missing tariff Cost 0 "Not priced (no tariff)" with an Add tariff action; a hole in a price history is a price gap (unavailable). The quantities stay visible. An explicit 0 tariff is still a valid zero (D-38)
Standing charges Per meter, per month that had readings, and copied onto every meter of the type Per local day over the scope's service period (including reading gaps), once per scope. Type and global charges are their own rows; meter fees stay on their meter (D-40, A-18)
Price of a longer bucket Month buckets used the price of the 15th, year buckets the price of 1 July Every bucket is priced month by month at the price of the 15th, so a year equals the sum of its months and changing the bucket never changes a total (D-36)
Months in which the billed grid meter was not yet (or no longer) in service — (0.3.0 summed every meter) Unavailable rather than free while use was measured, with an attention item naming the grid meter and the months (A-17)
Tank, runtime, direct-delta or instant-rate readings more than a month apart (a tank dipped every few months, quarterly burner hours) Booked whole in the month of the later reading, with zeros in between The months in between read "only coarser data" (not zero). A year or longer bucket is priced when all its months share one price; otherwise it is unavailable with an attention item (A-16)
A period that opens after the last reading of a meter read more coarsely than it (a tank dipped once a year, last dipped shortly before the period began) "No data for this period", beside a coverage panel listing years of data "Only coarser data", naming the resolution: the next dipstick will book it. The card, the chart, the table and the coverage panel now say the same thing, and the panel adds a measure's own dates when they differ from the type's. A meter that books its own buckets — an hourly source gone silent, a monthly sheet asked about a later month — still reads "no data" (A-41)
Automatic interval with data coarser than a month The whole chart went to years, which resolved nothing: "last 12 months" became one bar per year Months, with the buckets marked "only coarser data" and the coarser interval one click away. A meter read on the quarter, whose intervals lie inside one year, still charts in years (A-41)
Manual costs Counted in the Overview but not in the trend; a cost dated later this month counted at once Counted once, on their start day, when that day has come, everywhere: Overview, Analysis, categories, export (D-41)
Cost categories Sum of their member meters' costs The priced non-overlapping cover of their members plus their manual costs. Seeded Strom = Netz × price. A category overlapping another is shown as a view, apart from the composition. A category whose members price nothing says so (D-42, A-22)
Virtual meters No analysis; the flow summed incoming links, ignoring any formula Full analysis from the stored formula; the Sankey uses the same values (D-27, D-30)
Summe Solar and other generation sums Analysed as generation (seeded 2025: 4,750 kWh = Solar 1 3,123 + Solar 2 1,627) and not costed, because generation is never billed (A-15)
Readings at exactly midnight Booked in the following day Booked in the day they close (D-11)
"Last 12 months" 1314 buckets, including a partial future month 12 calendar buckets ending with the current month; actuals stop at now (D-02)
Rows dated after now Counted in "to date" totals Excluded and shown as "recorded after now", e.g. a sheet row labelled the current month or a future-stamped reading. A day that holds such a row reads partial (D-04, A-14, A-20)
Overview "this month / this year" Current month and year against the complete previous ones The selected period against the same elapsed part of the comparison period, measured over what both cover (D-07)
"Latest month with data" An amount without its month; consumption only The month and its basis (meter data, manual costs or both) (D-19)
Percentages A negative baseline was divided by its absolute value "Not applicable" for a zero or negative baseline; the absolute difference is always shown (D-08)
Meter dates Outside its install and retire dates a meter counts as a known zero; retired meters keep their history (D-24)
Currency € hard-coded in most views MeterVault__Currency everywhere; a tariff in another currency is reported as not fitting, never converted (D-43)
Continuous aggregates Refreshed hourly, read by nothing Dropped; rollup tables are written with each recompute (D-12, D-17)
Seed Adds the water price of 7.00 €/m³ from 2026-01-01, and stores Summe Solar's formula (m4 + m5, generation, not costed). This affects new seeds; existing seeded instances get the formula through the startup conversion (D-44)

REST API

Every existing field keeps its name and type. What changed is only added as new fields. The numbers follow the new engine, as listed above: actuals stop at now, virtual meters are evaluated, and costs are the bill's.

  • GET /api/v1/consumption:
    • New fields: status, issue, kind, unit.
    • A month without data is left out instead of reported as 0.
    • from/to with a UTC offset are accepted; they used to fail with a server error.
    • Virtual meters return evaluated values.
  • GET /api/v1/cost:
    • New fields: costStatus (Priced, Partial, NotPriced, PriceGap, UnitMismatch), costAvailability (the state of the quantities behind the cost, e.g. Unresolved, Invalid), costRule, notCosted, missingPrices[] (component, reason, scope, first and last month, tariff, unit issue, credit), status, issue, kind, unit.
    • cost stays numeric and is 0 when nothing could be priced; check costStatus and costAvailability before trusting a 0.
    • Behaviour change: generation and runtime meters, indicators and calculations that cannot be evaluated now report costStatus: NotPriced, with notCosted giving the reason. They used to report Priced, and a generation meter could carry a negative feed-in cost (A-21).
  • GET /api/v1/dashboard/summary:
    • New fields: deltaPercentApplicable per KPI, and latestMonth {period, basis}.
    • The month and year windows are unchanged (the calendar month and year to now, against the whole previous ones), but the values are the new bill. deltaPercent is 0 when not applicable.

Known limitations

  • Raw retention is not enforced (D-57). MeterVault__RawRetentionDays is shown but nothing deletes readings, because every recompute rebuilds a meter from its readings.
  • Monthly data is never interpolated to days (D-57). A day or week view of monthly data says "only coarser data" and offers the monthly view.
  • Bonus, Discount and Tax tariffs are stored but not applied (D-57).
  • Every live reading recomputes its meter in full (D-57). That is fine for monthly and daily meters, but costs about 1.2 s per reading for a meter with a year of hourly data, and grows with history.
  • Months cannot switch billing basis: the billing basis (grid meter or household use) is chosen per energy type for all time (A-17).
  • Batteries are not modelled. Without a grid-export meter, Solar's feed-in is calculated, and labelled as such.
  • No CSV export on the Solar page, because the export has no derived measures.