# Iași air quality > Hourly readings from the uRADMonitor air-quality sensor network deployed across Iași, republished by Portalul Open Data Iași (Primăria Municipiului Iași). The source sends each sensor as one wide object with a param/avg/last column triple per pollutant; this connector reshapes that into long format — one row per (sensor, reading_time, pollutant) — so pollutants can be filtered, aggregated and compared directly. - Dataset id: `iasi-air-quality` - Update cadence: hourly - Upstream source: https://opendata.oras.digital/dataset/cf3f-2309-44d1-8e0c-1137/ - Tags: air-quality, environment, sensors, pollution, romania, iasi, uradmonitor, weather - Current version: ingest `01M54PWS4ZP8EKTZGM1AX44MFM`, snapshot 2026-10-17, generated 2026-10-17T10:35:45.624Z ## Licence and attribution - Licence: Romanian official acts and public-interest information — outside copyright (`ro-official-acts-public-domain`) - Licence text: https://legislatie.just.ro/Public/DetaliiDocument/7816 - Required attribution: Date despre calitatea aerului publicate de Primăria Municipiului Iași — Portal Open Data - Notes: Legea 8/1996 privind dreptul de autor, art. 9, excludes from copyright both official texts of an administrative nature and simple facts and data. The pollutant readings republished here are bare factual measurements captured by municipal sensor hardware, and the sensor catalogue is the official record of that public monitoring network's own devices. They are also public-interest information under Legea 544/2001 and re-usable public-sector information under Legea 179/2022, which transposes the EU Open Data Directive. HOWEVER, and a reuser must weigh this: the portal brands itself "Open Data" but names NO licence anywhere it publishes. Verified directly: `https://opendata.oras.digital/terms/` (fetched 2026-09-01) is exclusively a liability disclaimer ("Primăria Municipiului Iași nu își asumă niciun fel de răspundere...") plus a cookie/Google Analytics policy — it contains no reproduction grant and no reproduction restriction, and the dataset landing page and API docs page carry no separate licence statement either. This dataset therefore rests on the legal position above rather than on any grant from Primăria Municipiului Iași — the "Open Data" branding is not itself a licence — and anyone republishing it should make their own assessment. This is a DERIVED work, not a republication: `sensors` carries the device's identity, location and hardware fields, and `readings` carries the per-pollutant average and last values — both reshaped from the source's wide per-pollutant JSON columns (param_X / avg_X / last_X, repeated once per pollutant) into one row per (sensor, reading_time, pollutant); nothing else the source exposes (e.g. the device photo URL) is republished. `robots.txt` on this domain is `User-agent: * / Allow: /` — unlike, say, curs.bnr.ro, nothing here signals that automated access is unwelcome. Finally, and most important for how this dataset should be used: the underlying uRADMonitor units are low-cost citizen-science-grade sensors, and their readings are indicative of local conditions, not calibrated regulatory reference measurements. Do not present a value from this dataset as an official air-quality compliance figure, and do not use it in place of a certified monitoring station's reading where the distinction matters (e.g. a legal exceedance finding). Reproduce the attribution line verbatim anywhere you republish this data. ## Manifest - Start here for how this directory works: https://data.bunnytech.app/llms.txt - This dataset: https://data-directory.fsn1.your-objectstorage.com/public/iasi-air-quality/manifest.json - All datasets: https://data-directory.fsn1.your-objectstorage.com/public/index.json - Human page: https://data.bunnytech.app/datasets/iasi-air-quality The manifest is the contract: file URLs, row counts, column types, min/max statistics and example queries all live there. Re-read it to discover a new version. ## Fastest path — Parquet exports Immutable, physically sorted snapshot exports. No catalog fetch, best row-group pruning, safe to cache forever. Register one view per table: ```sql INSTALL httpfs; LOAD httpfs; CREATE OR REPLACE VIEW readings AS SELECT * FROM read_parquet('https://data-directory.fsn1.your-objectstorage.com/public/iasi-air-quality/v/01M54PWS4ZP8EKTZGM1AX44MFM/readings.parquet'); CREATE OR REPLACE VIEW sensors AS SELECT * FROM read_parquet('https://data-directory.fsn1.your-objectstorage.com/public/iasi-air-quality/v/01M54PWS4ZP8EKTZGM1AX44MFM/sensors.parquet'); ``` Example queries: -- PM2.5 at one sensor over the last week: pollutant leads the sort, so this filters to a single row-group family before the time range and sensor filters narrow further. ```sql ATTACH IF NOT EXISTS 'ducklake:https://data-directory.fsn1.your-objectstorage.com/catalog/catalog.ducklake' AS "datadirectory" (READ_ONLY); SELECT reading_time, value_avg, value_last FROM datadirectory.iasi_air_quality.readings WHERE pollutant = 'pm25' AND sensor_id = 2181038179 AND reading_time >= current_date - INTERVAL 7 DAY ORDER BY reading_time ``` -- City-wide average PM2.5 per day: Every sensor with a valid PM2.5 reading, averaged per calendar day. ```sql ATTACH IF NOT EXISTS 'ducklake:https://data-directory.fsn1.your-objectstorage.com/catalog/catalog.ducklake' AS "datadirectory" (READ_ONLY); SELECT date_trunc('day', reading_time) AS day, avg(value_last) AS avg_pm25 FROM datadirectory.iasi_air_quality.readings WHERE pollutant = 'pm25' AND reading_time >= current_date - INTERVAL 30 DAY GROUP BY 1 ORDER BY 1 ``` ## Live lake — DuckLake catalog with time travel Attach the published catalog read-only over plain https. This works in native DuckDB AND in duckdb-wasm in the browser — the reader catalog resolves its data files through public https URLs, so no credentials and no backend are involved. ```sql INSTALL ducklake; LOAD ducklake; ATTACH 'ducklake:https://data-directory.fsn1.your-objectstorage.com/catalog/iasi-air-quality/catalog.ducklake' AS iasi_air_quality (READ_ONLY); SELECT * FROM iasi_air_quality.main.readings LIMIT 5; USE iasi_air_quality; ``` - Latest catalog: https://data-directory.fsn1.your-objectstorage.com/catalog/iasi-air-quality/catalog.ducklake - This exact version: https://data-directory.fsn1.your-objectstorage.com/catalog/iasi-air-quality/v/01M54PWS4ZP8EKTZGM1AX44MFM/catalog.ducklake (sha256 0cc7c929fa52c4d436bb7e1549743af512fb94c83b70fdee87f28ed7c0397829) - Written by DuckDB v1.5.5 - Time travel: this ingest is snapshot 30 — e.g. `SELECT * FROM iasi_air_quality.readings AT (VERSION => 30)` ## Wholesale — S3 access For bulk downloads, read the bucket prefix directly over S3 with httpfs: ```sql INSTALL httpfs; LOAD httpfs; CREATE SECRET hetzner ( TYPE s3, ENDPOINT 'fsn1.your-objectstorage.com', URL_STYLE 'path', KEY_ID '', SECRET '' ); SELECT * FROM read_parquet('s3://data-directory/public/iasi-air-quality/v/01M54PWS4ZP8EKTZGM1AX44MFM/**/*.parquet'); ``` ## Schema ### readings One row per (sensor, reading_time, pollutant): the average and most recent value a uRADMonitor air-quality sensor reported for one pollutant, as of that sensor's own last report time. Reshaped from the source's wide per-pollutant columns into long format so pollutants can be filtered, grouped and compared directly. A reading is never revised once published — the next report from the same sensor arrives with a later reading_time, not a correction of this one. - 4,561 rows · 23 KB · write mode `append` - Physically sorted by: `pollutant`, `reading_time`, `sensor_id` - Primary key: `sensor_id`, `reading_time`, `pollutant` - Files: 1 - https://data-directory.fsn1.your-objectstorage.com/public/iasi-air-quality/v/01M54PWS4ZP8EKTZGM1AX44MFM/readings.parquet (4,561 rows) Columns: - `sensor_id` BIGINT NOT NULL — The uRADMonitor device id this reading came from, decoded from its hexadecimal source id (e.g. "82000263") into a BIGINT. Joins to sensors.sensor_id. (range: "2181038691" … "2181038710") (~23 distinct) - `reading_time` TIMESTAMPTZ NOT NULL — The instant this reading is for (UTC) — the source sensor's own `timelast` (Unix epoch seconds, an unambiguous instant), not the time this connector happened to poll the API. Two sensors polled in the same run can carry different reading_time values when one of them has not reported in a while. (range: "2020-10-26 07:17:06+00" … "2026-10-17 10:35:19+00") (~350 distinct) - `pollutant` VARCHAR NOT NULL — Canonical pollutant code, taken verbatim from the source's own `param_` field name (e.g. "pm25", "co2", "temperature") — never a hand-maintained lookup table, so a pollutant the source adds shows up here automatically. (range: "ch2o" … "vocaqi") (~11 distinct) - `display_name` VARCHAR — Human-readable pollutant name as published by the source's own param_ metadata (e.g. "PM2.5", "CarbonDioxide"). Null when that metadata was missing or malformed for this particular reading — value_avg/value_last are still trustworthy in that case, only the label is unavailable. - `unit` VARCHAR — Unit of value_avg/value_last, parsed out of the source's own param_ metadata (e.g. "µg/m³", "ppm", "°C") rather than a hardcoded unit table, so a unit change upstream is reflected automatically. Null under the same conditions as display_name. - `value_avg` DECIMAL(18,4) — Average value the sensor reported for this pollutant as of reading_time. The averaging window is defined by uRADMonitor, not by this connector. Null when the source omitted or could not provide an average for this pollutant on this poll. - `value_last` DECIMAL(18,4) — Most recent instantaneous value the sensor reported for this pollutant at reading_time. Null when the source omitted it. - `flagged_error` BOOLEAN NOT NULL — True when the source's own `errors` field named this exact pollutant as faulty for this sensor at the time of this reading (uRADMonitor flags specific pollutant channels, not just whole devices, as erroring). value_avg/value_last are still published as reported — they are not dropped — but should be treated with suspicion when this is true. ### sensors One row per physical uRADMonitor unit in the Iași air-quality network, with its location and hardware details. Upserted: first_seen_date/last_seen_date widen over time, everything else reflects the most recent poll. - 20 rows · 4.3 KB · write mode `upsert` - Physically sorted by: `sensor_id` - Primary key: `sensor_id` - Files: 1 - https://data-directory.fsn1.your-objectstorage.com/public/iasi-air-quality/v/01M54PWS4ZP8EKTZGM1AX44MFM/sensors.parquet (20 rows) Columns: - `sensor_id` BIGINT NOT NULL — The uRADMonitor device id, decoded from its hexadecimal source id (e.g. "82000263") into a BIGINT so it sorts and joins numerically. Stable for the life of the unit. (range: "2181038691" … "2181038710") (~23 distinct) - `name` VARCHAR — Free-text description of the sensor's physical location, as entered by the network operator (typically a street intersection or landmark), e.g. "Bd.Independentei-Str.Sarariei". Cleaned of a literal leading two-character "\0" the source sometimes emits (see cleanNote in feed.ts) — that is an upstream artefact, not meaningful text. Null when the source sent nothing usable. - `latitude` DOUBLE [degrees] — Sensor location, WGS 84 latitude. - `longitude` DOUBLE [degrees] — Sensor location, WGS 84 longitude. - `altitude` DOUBLE [m] — Sensor elevation above sea level, as reported by the device. - `city` VARCHAR — City name as reported by the source. - `country` VARCHAR — ISO country code as reported by the source. - `placement` INTEGER — Installation placement code as reported by uRADMonitor. The portal publishes no legend for this code; kept verbatim (every unit observed so far reports 1) so a consumer can at least group identical installs, or map it once a legend surfaces. - `mobile` BOOLEAN — True when uRADMonitor flags this unit as mobile (vehicle-mounted) rather than a fixed street installation. Null when the source does not report the flag, which is the case for every fixed unit observed so far. - `status` INTEGER — Device status code as reported by uRADMonitor as of the most recent poll. The source publishes no legend for its values; cross-reference last_reading_at for staleness rather than relying on this code alone. - `versionsw` INTEGER — Firmware version running on the device. - `versionhw` INTEGER — Hardware revision of the device. - `owner` BIGINT — Numeric id of the uRADMonitor account that operates this device, as assigned by the uRADMonitor network — not a Portalul Open Data Iași id. - `detector` VARCHAR — Model of the unit's onboard Geiger-Müller radiation-detector tube (e.g. "SI29BG"). Unrelated to the air-quality pollutants published in readings — it identifies the physical hardware, which uRADMonitor units also use for background-radiation monitoring alongside air quality. - `first_reported_at` TIMESTAMPTZ — When this physical device first reported data to uRADMonitor (UTC), per the network's own records (source field `timefirst`, Unix epoch seconds). Not when this connector first observed it — see first_seen_date for that. - `last_reading_at` TIMESTAMPTZ — Timestamp (UTC) of the most recent measurement this device had reported to uRADMonitor as of the last poll (source field `timelast`, Unix epoch seconds). A value well in the past means the device has gone quiet; readings.reading_time for this sensor stops advancing past this point too. - `first_seen_date` DATE NOT NULL — The first run day (UTC) on which this connector saw this device. Maintained automatically by the upsert loader — never overwritten, only widened. - `last_seen_date` DATE NOT NULL — The most recent run day (UTC) on which the source still listed this device. ## Performance notes - There are no indexes in a lakehouse. The physical sort order plus Parquet row-group min/max statistics ARE the index. Predicates on the leading sort columns prune whole row groups; everything else is a full column scan. - `readings` prunes on `pollutant` → `reading_time` → `sensor_id`. - `sensors` prunes on `sensor_id`. - `ILIKE '%needle%'` cannot prune: it decompresses that whole column. Prefer prefix predicates on the lowercased search companion columns where they exist. - Every file URL under `/v//` is immutable. Cache it as aggressively as you like. - Files are never rewritten in place — a new ingest writes a new version path. Re-read the manifest to pick up updates rather than polling the file URLs. - Pull only the columns you need. Parquet is columnar and the reader range-GETs what it reads. ## Do not - Do not scrape https://data.bunnytech.app HTML. Every number on those pages comes from the manifest above. - Do not hit the upstream source (https://opendata.oras.digital/dataset/cf3f-2309-44d1-8e0c-1137/) in bulk. This mirror exists so you do not have to. - Do not read DuckLake internal data files with a plain Parquet reader. They come with positional delete files, and ignoring those silently returns deleted rows. Use the ducklake extension, or use the published exports above. - Do not republish without the attribution line, and do not imply endorsement.