Skip to content
Updated Sep 22, 2026 by Pablo Coufal · Owner: analysisactiveapi-a Edit on GitHub

Consumption Aggregation & Effective Consumption — API Analysis ​

One endpoint. It answers "how much, over this period" for a gauge or a set of them, which is the question the chart API deliberately does not answer: Chart Engine returns a series at a granularity, and a period that does not align to calendar buckets — a billing period from the 4th to the 4th — cannot be totalled from one without the caller doing the arithmetic. Totals are computed once, here, so the invoice screen, the invoice tolerance check and any report agree.

The invoice comparison itself is composed by Invoice Management: it holds the invoiced values, the previous invoice and the same period last year, and reads this endpoint for the reading-derived side.

The project has no shared API-conventions document yet, so the envelope shapes are stated once here. Success:

json
{
  "data": {},
  "status": 200,
  "message": "OK",
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b21"
}

Error:

json
{
  "errorCode": "ERR_CONSUMPTION_SUMMARY_INVALID_RANGE",
  "errorMessage": "from must be earlier than to.",
  "status": 400,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b21"
}

📥 GET /v1/consumption/summary ​

Totals consumption or cost over an arbitrary period for one gauge or a set of them, grouped as the caller asks — by tariff for an invoice comparison, by gauge or medium for a portfolio figure — with the coverage behind the number and a flag when a recalculation is still pending.

Authorization ​

Requires permission code tenant.consumption.read, tenant-scoped.

Request Headers ​

None beyond the application's standard bearer authentication.

Request Body ​

None.

Request Parameters ​

NameInTypeRequiredDefaultDescriptionValidation / NotesDB Mapping
gaugeIdQueryUUIDConditional-Gauge to total. Repeatable.At least one gaugeId or buildingId must be given; an id that does not exist in the tenant is an error, never a silently smaller sum.consumption.gaugeId
buildingIdQueryUUIDConditional-Building whose gauges are totalled, resolved to its gauge set with each sign applied once. Repeatable.As above. Richer selection — owner, manager, campus, class — is resolved by Chart Engine and passed here as gauge ids.gauge.buildingId
fromQueryTimestampYes-Start of the period, inclusive.ISO 8601. Need not align to a calendar boundary.consumption.slotAt
toQueryTimestampYes-End of the period, exclusive.ISO 8601; must be later than from.consumption.slotAt
kindQueryEnumNoconsumptionWhether to total a quantity or money.Enum - ConsumptionKind.consumption.kind
typeQueryEnumNoallLimits to one measured quantity.Enum - ReadingType. Omitted, the response separates quantities rather than adding them.consumption.type
directionQueryEnumNoallLimits to one flow direction.Enum - GaugeDirection.consumption.direction
tariffQueryEnumNoallLimits to one tariff band. Repeatable.Enum - ReadingTariff.consumption.tariff
sourceQueryEnumNoeffectiveTotals a named source series instead of the effective one. Repeatable; more than one value returns them side by side rather than added.Enum - ReadingSource. Omitted, the effective value is chosen per interval by the gauge's priority.consumption.source
groupByQueryEnumNononeHow the total is broken down. Repeatable: tariff, source, gauge, medium, type, usageType.Medium, quantity and unit always separate a group even when not asked for, so units never mix. usageType groups by the purpose of the gauge a value came from — heating, hot water, production, other — which is what a caller normalising only the heating share needs.gauge.purpose for usageType
aggregateQueryEnumNosumsum totals the period; max returns the largest single 15-minute value in it.A sizing question — what breaker or reserved capacity this point needs — is a maximum, not a total, and must not be re-derived by each caller.-
unitQueryEnumNocanonicalUnit the totals are returned in.Enum - Unit; must belong to the quantity's family.consumption.unit

Request Logic ​

  • Reads consumption and its summaries; writes nothing.
  • Whole days inside the period are read from the daily summary; the partial days at each edge are read from the 15-minute series, so a period that starts and ends mid-day costs one scan of two days rather than of the whole range.
  • With no source, the effective value is chosen per 15-minute interval before anything is added, so the total of a period with partial remote coverage equals no single source series.
  • With buildingId, the building resolves to its gauge set, each gauge's sign is applied once, and a gauge reachable twice is counted once.
  • Coverage is counted in the same pass: gauges asked for, gauges with any value, and intervals covered against intervals in the period.
  • A gauge and window still awaiting recalculation marks the affected totals provisional.
  • With aggregate=max, the largest 15-minute value in the period is returned per group, read from the series rather than from a summary, and value carries it with peakAt naming the interval it fell in.
  • A rolling window needs no special support: the caller computes the range — the last 365 days, the last 90 — and passes it as from and to.
sql
SELECT tariff, SUM(value) AS value, unit
FROM consumption
WHERE gauge_id = ANY(@gaugeIds)
  AND slot_at >= @from AND slot_at < @to
  AND kind = @kind
  AND source = @resolvedSource
GROUP BY tariff, unit;

Transactional Operations ​

N/A — read-only.

✅ Success Response (200) ​

The invoice comparison case: one electricity gauge, a billing period from 4 May to 4 June, grouped by tariff.

json
{
  "data": {
    "period": {
      "from": "2026-05-04T00:00:00Z",
      "to": "2026-06-04T00:00:00Z"
    },
    "totals": [
      {
        "medium": "electricity",
        "type": "energy",
        "tariff": "high",
        "value": 3612.000000,
        "unit": "kWh",
        "sources": ["remote"]
      },
      {
        "medium": "electricity",
        "type": "energy",
        "tariff": "low",
        "value": 1498.000000,
        "unit": "kWh",
        "sources": ["remote"]
      }
    ],
    "coverage": {
      "gaugesRequested": 1,
      "gaugesWithData": 1,
      "intervalsInPeriod": 2976,
      "intervalsCovered": 2976,
      "buildingsWithoutTotal": 0
    },
    "provisional": false
  },
  "status": 200,
  "message": "OK",
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b81"
}

Response Data Mapping ​

Response FieldSource / Value
period.from, period.toThe requested period, echoed so a stored answer stays self-describing
totals[].mediumgauge.medium of the contributing gauges
totals[].typeconsumption.type
totals[].tariffconsumption.tariff; present when tariff is in groupBy
totals[].valueSum of consumption.value over the group, after the per-interval source choice
totals[].unitconsumption.unit, or the requested unit after conversion
totals[].sourcesThe distinct consumption.source values that contributed — one entry where a single series covered the period, more where the effective total is a blend
coverage.gaugesRequestedCount of gauges the selection resolved to
coverage.gaugesWithDataCount of those with at least one value in the period
coverage.intervalsInPeriodQuarter hours between from and to
coverage.intervalsCoveredQuarter hours in which every contributing gauge had a value
coverage.buildingsWithoutTotalBuildings in the selection with no recognised total formula
totals[].peakAtThe interval carrying the maximum, present only with aggregate=max
provisionalTrue while a recalculation is pending for any gauge and interval inside the period

❌ Error Responses ​

400 ERR_CONSUMPTION_SUMMARY_NO_SELECTION ​

Thrown when neither gaugeId nor buildingId is given.

json
{
  "errorCode": "ERR_CONSUMPTION_SUMMARY_NO_SELECTION",
  "errorMessage": "At least one gaugeId or buildingId is required.",
  "status": 400,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b81"
}

400 ERR_CONSUMPTION_SUMMARY_INVALID_RANGE ​

Thrown when from is not earlier than to, or either is not a valid timestamp.

json
{
  "errorCode": "ERR_CONSUMPTION_SUMMARY_INVALID_RANGE",
  "errorMessage": "from must be earlier than to.",
  "status": 400,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b81"
}

400 ERR_CONSUMPTION_SUMMARY_INVALID_GROUPING ​

Thrown when groupBy names something that is not a grouping, or unit does not belong to the requested quantity's family.

json
{
  "errorCode": "ERR_CONSUMPTION_SUMMARY_INVALID_GROUPING",
  "errorMessage": "unit MWh cannot express a volume.",
  "status": 400,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b81"
}

404 ERR_GAUGE_NOT_FOUND ​

Thrown when a requested gauge or building does not exist in the tenant. A selection is never quietly reduced: a total over four gauges when five were asked for is indistinguishable from a real fall in consumption.

json
{
  "errorCode": "ERR_GAUGE_NOT_FOUND",
  "errorMessage": "Gauge not found.",
  "status": 404,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b81"
}

Internal access — the same totals without HTTP ​

Features in the same application read the totals through a port rather than over HTTP, so the arithmetic has one implementation:

summarise(selection, period, options) -> { totals, coverage, provisional }
  • Read-only; participates in the caller's transaction.
  • Takes the same options the endpoint exposes, and applies the same per-interval source choice before summing.
  • Used by Invoice Management for the invoice tolerance check, and available to Chart Engine where a caller wants a total rather than a series.