Asopitech With · Air quality monitor
Airthings air quality history with Alopex Skulk
Airthings is an air quality monitor that measures indoor air such as radon, particulate matter and VOCs and shows history graphs and notifications in an app. What if we rebuilt it with Alopex Skulk?
Updated: Oct 13, 2026
What the service does
Airthings View Plus uses several sensors to detect invisible airborne pollutants, including radon, particulate matter and VOCs. The app tracks radon and VOC levels and shows historical air quality graphs, local pollen updates and notifications. This page rebuilds the readings history and graphs.
Where the data lives
- Skulk time series: radon, particulate matter, VOC and other readings are appended to Skulk with room and device tags.
- Time-bucketed aggregation: daily and weekly averages come from time buckets and feed the graphs.
- PromQL: time above a threshold is queried with PromQL and drives notifications.
Reading values from the sensors and writing them is the job of the device and app code.
Deployment
Skulk is embedded in the monitor device and the phone app. Records collect locally, and where many buildings or homes are watched the same Skulk engine runs as a server or cluster to gather them.
Why Alopex Skulk fits
Air quality data is a numeric time series per sensor. Skulk handles ingestion, time aggregation and PromQL while embedded in the device or app. The stored data is Parquet, ready for long-term analysis as is.
See it in code
Air-quality readings are ingested into Skulk with Line Protocol and read back. Just the core (Rust).
use alopex_skulk::ingest::line_protocol::LineProtocolDecoder;
use alopex_skulk::ingest::{IngestLimits, Ingestor};
use alopex_skulk::model::FieldValue;
use alopex_skulk::store::recovery::{RecoveryConfig, RecoveryStore};
fn main() -> alopex_skulk::Result<()> {
std::fs::create_dir_all("airthings-data").unwrap();
let store = RecoveryStore::open("airthings-data", RecoveryConfig::default())?;
let limits = IngestLimits::default();
let mut ingestor = Ingestor::new(store, limits);
// CO2 の計測を Line Protocol で取り込む(measurement,tag field timestamp[ns])
let base: i64 = 7_200_000_000_000;
let now = base + 300_000_000_000;
for (i, co2) in [450.0, 620.0, 800.0].iter().enumerate() {
let ts = base + (i as i64) * 60_000_000_000;
let line = format!("air,room=living co2={co2} {ts}");
let batch = LineProtocolDecoder::new(limits).decode(line.as_bytes(), now)?;
assert_eq!(ingestor.ingest(batch, now)?.accepted_count(), 1);
}
ingestor.sink_mut().flush_all()?;
// 取り込んだ CO2 を読み出す
for row in &ingestor.sink().read_measurement("air")? {
if let Some(FieldValue::Float(co2)) = row.row().field("co2") {
println!("t={} co2={co2}", row.row().timestamp());
}
}
Ok(())
}Ingest is durable at the WAL boundary when ingest() returns. Daily and weekly averages and threshold-crossing windows come from Skulk’s PromQL / time-bucket aggregation.
Build air quality graphs with Alopex Skulk, which stores and aggregates sensor time series inside the device.