Temporal aggregation grouping
Group a temporal aggregation by calendar month, by hour of the day, by unit, or by the value another attribute had at the time. aggrGroupBy=unitCode,observedAt;month gives the average speed per calendar month, split by the unit each reading carried. Off by default.
At a glance
- Setting:
extensions.temporal_aggregation_grouping.mode,"disabled"(default) or"enabled" - Environment variable:
ANDROMEDA__EXTENSIONS__TEMPORAL_AGGREGATION_GROUPING__MODE - Advertised as:
temporalAggregationGroupingincontextSourceExtras.extensions - Context sources: never forwarded. Sources get a plain aggregated query, and Andromeda groups the merged answer.
- When off:
aggrGroupByandformat=groupedValuesget400InvalidRequest
Turning it on
[extensions.temporal_aggregation_grouping]
mode = "enabled"
max_groups = 10000
extensions.temporal_aggregation_grouping.max_groups (default 10,000) is the most distinct groups one grouped aggregation may produce. A request that would produce more gets 403 TooManyResults, and nothing is returned.
Using it
GET /ngsi-ld/v1/temporal/entities?type=Vehicle&timerel=after&timeAt=2026-01-01T00:00:00Z&format=groupedValues&aggrMethods=avg&aggrGroupBy=unitCode,observedAt;month
{
"id": "urn:ngsi-ld:Vehicle:1",
"type": "Vehicle",
"speed": {
"type": "Property",
"avg": {
"KMH": {
"2026-01-01T00:00:00Z": [
[
54.2,
"2026-01-01T00:00:00Z",
"2026-02-01T00:00:00Z"
]
]
},
"HM": {
"2026-01-01T00:00:00Z": [
[
31.0,
"2026-01-01T00:00:00Z",
"2026-02-01T00:00:00Z"
]
]
}
}
}
}
The extension adds two things to GET /temporal/entities, GET /temporal/entities/{id} and POST /temporal/entityOperations/query (as aggrParams.aggrGroupBy there):
| Parameter | Value |
|---|---|
format or options | groupedValues, the grouped version of aggregatedValues |
aggrGroupBy | one or more grouping terms, described below |
aggrGroupBy needs groupedValues and at least one aggrMethods value. The shape of the response depends on the format alone.
Writing aggrGroupBy
A term is a key, optionally followed by ; and a bucket. Terms are separated by ,, and each one nests inside the groups of the one before it.
The key can be:
- an attribute path, written as in
qororderBy, such aszone,address[city]orrefDevice.category datasetIdorunitCode, which belong to each temporal instanceid,typeorscope, which are allowed but give one group per entity, since they don't change within one entity's history
A bucket goes on a temporal key such as observedAt:
year,month,week,dayorhourcut the time into calendar periods.dayOfWeek,hourOfDayormonthOfYearfold it into cycles, soobservedAt;hourOfDayputs every reading between 08:00 and 09:00 into one group, whatever the day.
The full grammar:
AggregationGrouping = GroupingTerm *("," GroupingTerm)
GroupingTerm = GroupKey *1(";" BucketTerm)
GroupKey = ValuePath / EntityMember
EntityMember = "id" / "type" / "scope" / "datasetId" / "unitCode"
BucketTerm = "year" / "month" / "week" / "day" / "hour" ; calendar (contiguous)
/ "dayOfWeek" / "hourOfDay" / "monthOfYear" ; cyclic (non-contiguous)
How grouping works
aggrPeriodDuration still comes first. The history is cut into periods, and each period is then grouped by aggrGroupBy. Without a period, the whole range is one period. The order is: attribute and datasetId, then each aggrGroupBy term, then the period.
An attribute path groups each instant by the value that attribute had in effect at that instant: its latest instance not after the one being aggregated, on the same time axis. That is how you get "the average speed by the zone the vehicle was in".
Grouping by unitCode makes a mixed-unit period answerable. A plain aggregation refuses avg over kilograms and pounds (see Temporal aggregation), but with one group per unit, each group holds one unit and the period is split instead.
The response
groupedValues keeps the array of entities with each entity's id and type. Between each method member and its [value, start, end] triples, it adds one map per grouping term, keyed by the group's value. In the example above, unitCode is the outer map and observedAt;month the inner one. A calendar bucket is keyed by its first instant, such as 2026-01-01T00:00:00Z for January. An absent key is @none, and an explicit NGSI-LD Null is urn:ngsi-ld:null. Groups are ordered as orderBy orders values, under the query's collation.
Paging
A grouped answer is paged like an aggregated one: by whole periods, at most temporal.query.max_last_n periods and temporal.query.max_aggregation_instances instances per attribute and page, with every group of a period on the same page. Without a period, the whole range is one period and isn't paged. The group limit counts the groups on one page.
What gets refused
Context sources
Only the broker that receives the request groups. The temporal query goes to the registered sources as a plain aggregated query (groupedValues becomes aggregatedValues), their answers are merged, and Andromeda groups and aggregates the result. aggrGroupBy is never forwarded.
Why it is an extension
The specification fixes the group of a temporal aggregation at entity, attribute, datasetId and a fixed-width period, and lets you change none of it. Three things follow:
aggrPeriodDurationtiles the window fromtimeAt.P1Mgives month-long periods starting on the day oftimeAt, so they line up with calendar months only whentimeAtis the first of a month. And there's no way to ask for "every reading between 08:00 and 09:00, across all days".unitCodesits on the instance, so one Property can hold values in different units, and a period that mixes them can't be averaged.- Grouping by the value another attribute had at each instant can't be expressed at all.
The grouping grammar is modelled on the entity ordering language, and attribute paths are the same paths q uses.