Airthings

The Airthings connector brings indoor air quality, occupancy, and environmental telemetry from Airthings devices into the Mapped graph. It uses Airthings locations, segments, and devices to build a place and device model, receives live measurements through persistent webhooks, and can run an on-demand backfill job to import historical samples for the same points.
Use Cases
- Indoor air quality monitoring: Track CO₂, humidity, temperature, radon, particulate, TVOC, pressure, light, and sound measurements from Airthings sensors.
- Occupancy insights: Capture occupancy count values from supported devices alongside the spaces and zones they serve.
- Historical analysis: Backfill prior Airthings measurements into the same points used by the live webhook feed for trend analysis and reporting.
Configuration
Auth Requirements
The connector authenticates with Airthings using OAuth2 client_credentials.
| Field | Required | Description | Where to Find |
|---|---|---|---|
| Client ID | Yes | Airthings API client ID used to request OAuth access tokens | Your Airthings API application in the Airthings developer portal |
| Client Secret | Yes | Airthings API client secret paired with the Client ID | Your Airthings API application in the Airthings developer portal |
Place Mappings
This connector uses the connector entity framework and exposes three entity selection sections: Locations, Segments, and Devices.
Locations
Locations come from the Airthings Location resource and are materialized as Mapped buildings.
| Field | Required | Source | Description |
|---|---|---|---|
| Name | Yes | Airthings | Building name from the Airthings location |
| Postal Address | Yes | User (pre-filled when available) | Building address used for the Mapped building |
| Site | Yes | User | Existing Mapped site that will own the building |
Segments
Segments come from the Airthings Segment resource and represent rooms or spaces inside a building.
| Field | Required | Source | Description |
|---|---|---|---|
| Name | Yes | Airthings | Segment name |
| Space Code | No | Airthings label / User | Optional room code used to contribute to an existing room in the graph |
| Floor | Yes | User | Existing Mapped floor that will contain the segment |
Note: Airthings does not provide a first-class floor resource, so floors must already exist in the Mapped graph and are selected by the user.
Devices
Devices come from the Airthings Device resource. Hub devices are excluded; sensing devices are imported.
| Field | Required | Source | Description |
|---|---|---|---|
| Name | Yes | Airthings | Device product name |
| exactType | Yes | Connector default | AIR_QUALITY_SENSING_DEVICE |
| Location | Yes | Airthings / User | Target Mapped space or zone for the device. This is pre-filled when the reported Airthings segment already exists in the graph. |
Enumerations or Other Options
This connector does not require user-defined enumeration mappings.
It also provides an on-demand backfill job for historical telemetry with these user-facing options:
- Devices — all mapped devices or only specific serial numbers
- Date Range — history window to import
- Resolution — RAW, HOUR, FOUR_HOURS, DAY, THREE_DAYS, or WEEK
Advanced Options
| Option | Default | Description |
|---|---|---|
| Materialize mapped spaces as Zones | false | When enabled, imported Airthings segments are created as Zone vertices instead of Space vertices. Devices can still be located on them, and the connector still creates an additional child zone per device. |
| Temperature Unit | Celcius | Controls whether temperature points are created in DEG_C or DEG_F, and aligns webhook/backfill temperature ingestion to the same unit. |
Mapped Concepts
API to Mapped Entities
| Airthings Source | Mapped Entity | exactType | Relationship |
|---|---|---|---|
| Location | Building | Building | isPartOf → Site |
| Segment | Space (default mode) | Space | isPartOf → Floor |
| Segment | Zone (when Materialize mapped spaces as Zones is enabled) | Zone | isPartOf → Floor |
| Device | Thing | AIR_QUALITY_SENSING_DEVICE | hasLocation → imported segment place |
| Derived per-device zone | Zone | Zone | hasPart from the imported segment place; isServedBy from the device |
API to Mapped Points
Points are created on each Airthings device according to the sensor list reported for that device. Live webhook events and backfill history both write into the same device points.
| Airthings Measurement | Mapped Point | Datatype | Unit | Description |
|---|---|---|---|---|
| co2 | CO2LevelSensor | DOUBLE | PPM | Carbon dioxide concentration |
| humidity | HumiditySensor | DOUBLE | PERCENT_RH | Relative humidity |
| pm1 | PM1LevelSensor | DOUBLE | PPB | PM1 concentration |
| pressure | PressureSensor | DOUBLE | HectoPA | Air pressure |
| occupants | OccupancyCountSensor | DOUBLE | NUM | Occupancy count reported by the device |
| pm10 | PM10LevelSensor | DOUBLE | PPB | PM10 concentration |
| pm25 | PM25LevelSensor | DOUBLE | PPB | PM2.5 concentration |
| light | IlluminanceSensor | DOUBLE | LUX | Illuminance |
| temp | AirTemperatureSensor | DOUBLE | DEG_C or DEG_F | Temperature, based on the configured temperature unit |
| radonShortTermAvg | RadonConcentrationSensor | DOUBLE | BQ-PER-M3 | Short-term average radon concentration |
| voc / tvoc | TVOCSensor | DOUBLE | PPB | Total volatile organic compounds |
| sla / soundLevelA | SoundPressureLevelSensor | DOUBLE | DeciB | Sound pressure level, when the device exposes this measurement |
Sample Code
There are a few likely ways you'd want to retrieve the air quality data - first would be if you knew which sensor in specific you wanted to look up:
{
things(filter: {id: {eq: "THGUeKkU5s1Lyg4sSieSg1abC"}}) {
id
name
exactType
points {
id
name
unit {
name
}
series(latest: true) {
timestamp
value {
float64Value
}
}
}
}
}
An alternative might be to look up a specific space the sensor is located in:
{
spaces(filter: {id: {eq: "SPCLLKzRU2zqrQU2KSnmbabcD"}}) {
id
name
things {
id
name
points {
id
name
exactType
series(latest: true) {
timestamp
value {
float64Value
}
}
}
}
}
}
Note that in some cases, an occupancy sensor may not be assigned to a specific space - zone is another possibility:
{
zones (filter: {id: {eq: "ZONEMwCKzKpgaNeUwuihwiABcd"}}) {
id
name
things {
id
name
points {
id
name
exactType
series(latest: true) {
timestamp
value {
float64Value
}
}
}
}
}
}