Appearance
Consumption Calculation
Business Context
Business-Level Definition
An energy manager does not work with meter states; they work with consumption. A meter state is the number on the display, and on its own it answers nothing. Consumption is the difference between two states, corrected by the meter's coefficients, spread across the period between them, and expressed in a unit people compare in.
This feature is the step that turns readings into consumption. It is the only place where that conversion happens, so that a figure shown in a chart, checked against an invoice, compared with last year or used to calculate emissions is the same figure everywhere, derived the same way.
Requirements Definition
Everything the product promises an energy manager rests on this step: without it there is no chart, no report, no invoice check, no benchmark, no saving and no emission figure — only raw numbers nobody can act on.
Three things make it more than a subtraction. A metering point is often measured more than one way — read remotely, read by hand, and billed — and the three disagree; each way has to be kept whole and comparable rather than merged into one lossy number. Gas is measured in cubic metres but managed in kilowatt-hours, and the conversion depends on coefficients that change over time, so the same volume converts differently in January and in June. And corrections are normal: a mistyped reading or a corrected coefficient must change history, quietly and completely, or two people reading the same screen a week apart will see different numbers and trust neither.
Technical Context
User Stories / Use Cases
- As an energy manager, I want the consumption of a metering point for a period I choose, so I can see what it used.
- As an energy manager, I want a gauge that is read both remotely and by hand to give me either series, so I can see whether they agree.
- As an energy manager, I want gas shown in kilowatt-hours, converted with the coefficients that were valid at the time, so a cold January is not distorted by today's calorific value.
- As an energy manager, I want a corrected reading or coefficient to correct the past as well, so historic comparisons stay true.
- As an energy manager, I want a period with no measurement to read as unmeasured rather than as zero, so I do not mistake a gap for a saving.
- As a facility manager, I want consumption of a whole building, with own generation counted the way the meters are configured, so I can see what the building really used.
- As a facility manager, I want a building total to use the formula that was in force in the period I am looking at, not the one in force today.
UI/UX Design
N/A — the feature has no interface of its own. Its results are displayed by Chart Engine, Invoice Management and Building Management, each of which owns its own screens.
Functional Requirements
Series identity
- Compute and store consumption separately per gauge, measured quantity, flow direction, tariff band and source.
- Never form a difference across two sources: a manual reading is differenced against the previous manual reading, a remote value against the previous remote value.
- Keep every source's series; a later source never overwrites an earlier one.
- Reserve the
calculatedsource for the output of virtual gauges.
From reading to consumption
- For a cumulative series, compute the difference between two consecutive states and multiply it by the volume coefficient valid for the period.
- Select the volume coefficient by the source of the series being computed: a gauge keeps one validity history per reading source, so the manual series is corrected by the manual history and the remote series by the remote one. The two are never substituted for each other, even where only one of them has a value.
- Apply no correction where the gauge has no coefficient for that source and period: a volume coefficient corrects a meter that needs correcting, and its absence means none is needed. This is not the same as a missing calorific value, which leaves energy uncomputed.
- Compute the invoiced quantity series without a volume coefficient: an invoiced quantity is already a quantity, in the unit the supplier billed.
- Treat the cumulative register of one source and tariff as a piecewise-linear function of time between consecutive readings, and take each 15-minute value as the difference of the register at the interval's boundaries; readings need not be aligned to the quarter hour, and a reading finer than a quarter hour contributes only through the boundaries. Two manual readings therefore spread evenly by time; an aligned remote value is stored as it is; an hourly remote reading yields four equal quarters. The quarter hours holding the first and the last reading of a series carry their partial share, so a series always sums to the register difference.
- Distribute a
deltaoraveragereading over the intervals between the previous reading of the same source and this one, by time overlap. - Spread a value across intervals by cumulative rounding — each interval's value is the difference of the rounded cumulative totals at its boundaries (6 decimals for quantities, 2 for money) — so that the series sums exactly to the value spread and any contiguous run of intervals sums to the rounded share.
- Convert a volume quantity to energy as volume × volume coefficient × calorific value, using the values valid in the period being computed.
- Resolve the calorific value in the order gauge, client, system; use no implicit fallback beyond the system value.
- Convert a power reading to energy as power × interval length, keeping the source of the input reading.
- Prefer directly measured energy over energy derived from power where the same source supplies both for the same interval.
- Store gas consumption in kilowatt-hours and derive the volume figure for display from the coefficients valid in the period.
- Write no row for an interval with no input.
- Stop the calculation at a meter replacement: the closing reading ends the old period, the opening reading starts the new one, and no difference is formed across the boundary.
Virtual gauges and building totals
- Evaluate a virtual gauge's formula for each 15-minute interval, taking each member's effective consumption as the input, and store the result as a series of its own with source
calculated. - Apply each member's sign as its formula term states: included values add, subtracted values subtract, excluded and report-only members do not enter the result.
- Evaluate with the formula version valid for the interval being computed, not the version in force today.
- Store one total per building and medium, evaluated the way any other virtual gauge is.
- Compute a building total from its members' series, never from the readings behind them: a total is one level above its members, not a second derivation of the same inputs.
- Write no value for an interval in which any member has none, so a total is never completed from part of its members.
- Evaluate a virtual gauge only once every member it depends on is current, and treat a formula that would close a dependency cycle as invalid.
- Keep
calculatedoutside the source priority: it is the output of a formula, not a competing account of the same measurement, and a difference of two remote readings staysremote.
Corrections
- Recompute the affected series when a reading is added, changed or removed, limited to the gauge and the intervals the change touches.
- Recompute the affected series from the validity date forward when a volume coefficient or a calorific value changes.
- Publish a recalculation-in-progress state per gauge and period, so a surface displaying an affected figure can mark it provisional; it is returned with the period totals that cover it.
- Make a recalculation idempotent: running it twice over the same inputs produces the same rows.
Internationalization & Localization
N/A — the feature produces numbers and no user-facing text. Unit names, number formats and the choice of display unit belong to the surfaces that present the result.
Non-Functional Requirements
- Reproducibility. The whole series for a gauge can be rebuilt from its inputs at any time, and rebuilding it changes nothing else.
- Reliability. A failed recalculation leaves the previous values in place; it never leaves a series half-written.
- Scalability. Write volume is one row per gauge, quantity, source and quarter hour, and the design assumes both the number of gauges and the length of history grow.
Performance
| Path | Expectation | How it is met |
|---|---|---|
| Manual reading saved | Consumption visible immediately | Computed on the write path for the intervals the reading closes |
| Remote batch received | Consumption present within 30 seconds of receipt | Computed per batch, asynchronously; the lag is measured |
| Coefficient change | Recalculation proportional to the affected period, not the whole history | Bounded by gauge and by validity date |
Transactional Operations
The calculation writes one series for one gauge and one period atomically: either every interval in the recomputed window is present with its new value, or none of it changed. A recalculation request raised by a reading or coefficient change is enqueued in the same transaction as the change itself, so a committed change can never lose its recalculation, and a rolled-back one never schedules work for a value that was not stored. Concurrency on the same gauge and window is made safe by the unique index that carries the series identity: a second writer upserts onto the same rows rather than duplicating them.
Processes & Related Systems / Components
| Trigger | Owner of the trigger | What this feature does |
|---|---|---|
| Manual reading created, edited or deleted | Reading Management | Recompute the manual series for the affected intervals |
| Remote batch ingested | Remote Connection Management | Compute the remote series for the intervals in the batch |
| Invoice saved, corrected or deleted | Cost Calculation | Recompute the invoiced quantity series for the billing period |
| Volume coefficient added or voided | Gauge Management | Recompute the series of that reading source only, from the validity date forward |
| Calorific value changed | Client Management | Recompute the affected energy series from the validity date forward |
| Formula version added or corrected | Gauge Management | Recompute that virtual gauge's series from the version's validity date forward |
| Gauge output mode, direction or purpose changed | Gauge Management | Recompute once the resulting formula version is accepted |
| Gauge moved to another building | Building Management | Recompute the totals of both buildings |
| Source priority changed | Gauge Management, Client Management | Change no stored series; recompute the virtual series that consumed that gauge |
| Meter replaced | Meter Replacement Flow | Close the old period and start the new one; form no difference across the boundary |
| Series stored or recomputed | this feature | Hand the affected window to Consumption Aggregation & Effective Consumption |
Recalculation and propagation
Every stored series is derived from something else, so a change anywhere has to reach everything built on it. The unit of invalidation is one gauge and one window of intervals — never a single value, never a whole gauge — and an invalidation is written in the same transaction as the change that caused it, so a committed change cannot lose its recalculation. Each level emits its own invalidation as it finishes, which is how a corrected reading reaches a building total without either step knowing about the other.
| Change | What it invalidates | How far it travels |
|---|---|---|
| Reading added, changed or removed | that gauge's series of that source, for the touched intervals | virtual gauges that have it as a member, their totals, the summaries over both |
| Remote batch received | the same, bounded by the batch window | as above |
| Invoice saved, corrected or deleted | the invoiced series for the billing period | as above |
| Volume coefficient added or voided | that gauge's series of that reading source, from the validity date | as above |
| Calorific value added, corrected or withdrawn | the energy series of the gauges that resolve to it, from the validity date | as above |
| Source priority changed | nothing stored — only the effective view, and only in intervals where more than one source has a value | the virtual series that consumed that gauge |
| Formula version added or corrected | that virtual gauge's series, from the version's validity date | its dependents and their summaries |
| Gauge output mode, direction or purpose changed | the building total's formula, through a new version | as for a formula version |
| Gauge moved to another building | the totals of both buildings | as for a formula version |
| Reading retention thinning | nothing | — |
Four properties keep the cascade finite. Recomputation is an idempotent upsert over the window, so running it twice changes nothing. Invalidations for the same gauge and window are coalesced, so a bulk import produces one recomputation per gauge rather than one per reading. Evaluation is topological — a virtual gauge is evaluated once every member it depends on is current — which is why a cyclic formula is invalid. And atomicity is per level, not across the chain: a total may briefly be older than its members, which is what the recalculation-in-progress state exists to show, and it is published for dependents as well as for the gauge that changed.
A failed recomputation keeps the previous values and leaves its gauge and window marked as pending, so a stale figure is never presented as current and the work is retried rather than lost.
Nothing above the building is refreshed, because nothing above the building is stored: a figure for a set of buildings is summed when it is read — see Consumption Aggregation & Effective Consumption.
Diagrams & Models
Computing one series from one new manual reading:
sequenceDiagram
actor U as Energy manager
participant R as Reading Management
participant C as Consumption Calculation
participant G as Gauge coefficients
participant S as Consumption series
U->>R: Save reading (state, time)
R->>C: Reading stored
C->>C: Find previous reading of the same series
C->>G: Coefficients for this source, valid in the period
G-->>C: Volume coefficient of that source, calorific value
C->>C: Difference, convert, spread over intervals
C->>S: Upsert the interval values
C-->>R: Series updated
Choosing what to compute for one input value:
flowchart TD
A[Input value for a gauge] --> B{Reading type}
B -->|power| C[Energy equals power times interval length]
B -->|energy| D[Use the value as energy]
B -->|volume| E[Volume times the coefficient of this source<br/>times the calorific value]
C --> F{Directly measured energy<br/>from the same source?}
F -->|yes| G[Discard the power-derived value]
F -->|no| H[Keep the derived energy]
D --> H
E --> H
H --> I{Cumulative series?}
I -->|yes| J[Difference against the previous value of the same source]
I -->|no| K[Use the value for its own interval]
J --> L[Spread evenly over the intervals between the two readings]
K --> M[Store one value per interval and source]
L --> M
What a change reaches:
flowchart LR
A[Reading or invoice] --> B[Series of that source]
B --> C[Effective consumption<br/>read-time]
C --> D[Virtual gauge series<br/>source calculated]
D --> E[Building total<br/>a virtual gauge]
E --> F[Hourly, daily, monthly,<br/>yearly summaries]
F --> G[Read-time sums over<br/>a set of gauges]
H[Coefficient or<br/>calorific value] --> B
I[Source priority] --> C
J[Formula version] --> D
API Analysis
N/A — the feature has no HTTP surface of its own. It is driven by the events in Processes & Related Systems / Components above. Its series are served as charts by Chart Engine, as invoice figures by Invoice Management, and as period totals by Consumption Aggregation & Effective Consumption, where the recalculation state below is also exposed.
Domain Model (ER diagram) & Data Attribute Table
erDiagram
GAUGE ||--o{ READING : "is read as"
GAUGE ||--o{ VOLUME_COEFFICIENT : "is corrected by"
GAUGE ||--o{ CONSUMPTION : "consumes"
GAUGE ||--o{ CALORIFIC_VALUE : "may state"
CLIENT ||--o{ CALORIFIC_VALUE : "states"
READING ||--o{ CONSUMPTION : "is differenced into"
CALORIFIC_VALUE ||--o{ CONSUMPTION : "converts"
INVOICE ||--o{ CONSUMPTION : "bills"
- consumption — created by this feature; one value per gauge, interval and source, including the series of virtual gauges.
A virtual gauge's series is the virtual gauge's consumption. It is not the building's calculated consumption held in buildingCalculatedConsumption, which is a separately recorded reference figure from an audit or a user snapshot. The two names are close and the things are unrelated.
- reading — the input states and interval values.
- gauge — the metering point, its medium, its unit and its source priority.
- volumeCoefficient — the meter correction valid in a period, per reading source.
- calorificValue — the gauge and client scopes of the calorific value the conversion uses; the platform scope is defaultCalorificValue.
- invoice — the billed quantity that forms the invoiced series.
Data
No configuration data of its own. The calculation needs a volume coefficient only where the meter needs correcting, and then one per reading source the gauge is read by; and a calorific value only where a volume quantity is converted to energy. Both are maintained as gauge and client data, not as configuration of this feature.
A representative stored result for one gauge, one quarter hour, read both ways:
sql
INSERT INTO consumption
(gauge_id, slot_at, kind, type, direction, tariff, source, value, unit)
VALUES
('018f…', '2026-01-15T09:15:00Z', 'consumption', 'energy', 'import', 'high', 'remote', 12.480000, 'kWh'),
('018f…', '2026-01-15T09:15:00Z', 'consumption', 'energy', 'import', 'high', 'manual', 12.500000, 'kWh');Test Data
Use the project's Test Data location named in the overlay — the demo gauge seed is the fixture that produces gauges with readings. Search it for the gauge medium and reading source combinations this feature branches on: a gas gauge with a volume coefficient, an electricity gauge with tariff bands, and a gauge carrying both manual and remote readings over the same period with a different coefficient on each of its two histories — that last one is what proves the histories are not substituted for each other.
Worked cases with inputs, the calculation step by step, the resulting consumption rows and the check sums, one per Confluence example and per reading mode: calculation-cases.md. The rules that document settled (R-1 to R-11) are listed at its end with the artefacts they change.
Logging
The project has no shared logging-conventions document yet, so this feature's events are stated in full here.
.info— series computed for a gauge and window: gauge, quantity, source, interval count, trigger,requestId..info— recalculation started and finished: gauge, window, reason, duration..warn— an input could not be converted: gauge, interval, which coefficient or calorific value was missing..warn— a difference was discarded because it crossed a meter replacement boundary..error— a recalculation failed and the previous values were kept: gauge, window, cause.
Monitoring
Two feature-level needs: the lag between a remote batch arriving and its consumption being present, measured against the 30-second expectation above; and the count of recalculations that ended in error, which is the signal that a series is silently stale.
Caching
None. The series is read through the aggregation layer, which is where the cost of repeated reads is answered; caching a second copy here would add a way for two readers to disagree.
Backward Compatibility and Migration
Consumption is not migrated. It is recomputed from migrated readings and invoices, so the legacy daily figures are reproduced by the same rules that govern new data rather than carried across as numbers nobody can re-derive. Two consequences are expected and accepted: a legacy period whose readings are coarser than a quarter hour produces an evenly spread series, and a legacy figure that relied on a coefficient nobody recorded is recomputed without it and will differ.
Migrated readings that carry a date but no time are stored at midnight for clients whose legacy setting said so, which keeps a day's consumption on the day it was billed.
Legal Context
N/A — no contract, licence or regulation applies to this step. The figures it produces are not a billing document: the legal weight sits on the invoice, which Cost Calculation covers. Metrological certification of the meters themselves is an obligation of whoever operates them, discharged outside this product, and the feature neither asserts nor checks it.
Cybersecurity Considerations
Consumption is tenant business data and carries the tenant on every row. Because the table is a hypertable, database row-level security is not available, and isolation is enforced by the application-level guard that requires the tenant predicate on every statement — the same mechanism, and the same failure mode to watch, as the readings it derives from.
Data Privacy. No personal data. Consumption describes a metering point, not a person; the actor columns name the calculation that wrote the row, not a user.
Risk Assessment
Business Risks
A wrong figure here is wrong everywhere downstream and is not visibly wrong: a coefficient taken from the wrong reading source, or a conversion missing its calorific value, shifts consumption by a plausible-looking margin rather than producing an obvious error. The mitigation is that the series is reproducible — a suspected figure can be recomputed from its inputs and compared — that every series records which source it came from, and that a conversion without its calorific value is logged rather than silently defaulted.
The second risk is delay. If recalculation falls behind, screens show stale figures that look current; this is why the recalculation-in-progress state is published and the lag is monitored.
Technical Risks
| Risk | Consequence | Mitigation |
|---|---|---|
| Recalculation storm after a bulk correction | Backlog delays all series, not just the corrected ones | Recalculation is bounded by gauge and period, and enqueued rather than run inline |
| A correction arrives while the same window is being recomputed | Two writers on the same rows | Series identity is a unique index; writes are upserts |
| Missing calorific value for a volume-to-energy conversion | Energy cannot be computed for the period | No implicit default; the interval carries no row and the gap is logged and visible |
| A gauge's two coefficient histories are conflated | One series is corrected by the other's factor, shifting it silently | Selection is keyed by the series' reading source; a source with no coefficient is left uncorrected rather than borrowing the other's |
| Input retention removes the readings behind old consumption | Old series can no longer be rebuilt from source | Retention applies to inputs only; the computed series and its aggregates are kept indefinitely |
| A formula closes a dependency cycle | Evaluation cannot terminate | A cyclic formula is invalid and is rejected where formulas are saved |
| A member has no value for an interval | A total silently counts part of its members | No total value is written for that interval at all |
| Cascade — this feature stops | Charts, reports, invoice checks, benchmarking, savings and emissions all stop gaining new data | Failures leave previous values intact, so existing history stays readable |
Auditing, Reporting & Measurement
Consumption rows are not themselves audited: they are derived, and the audit trail that matters is the one on their inputs — the reading action log for readings, the invoice trail for invoices, and the append-only coefficient history for coefficients. Each row names the calculation that wrote it and when it was last recomputed, which is enough to trace a figure back to the change that produced it.
The feature's own measurement is the pair named under Monitoring: recalculation lag, and recalculation failures.