| name | update-air-quality |
| description | Refresh the air-quality monitoring-station data (data/air/index.json) by fetching the latest quarterly PM10 + PM2.5 CSVs from ИАОС via data.egov.bg. Use when the daily watch report flags `iaos_air_quality` as changed, when the user asks to refresh air quality / atmospheric monitoring / ИАОС data, when adding a new pollutant (NO₂ / O₃ / SO₂ / CO) to the index, or after a fresh git clone if data/air/index.json has zero stations. |
| allowed-tools | ["Read","Bash","Edit","Write"] |
Update air quality skill
Refreshes data/air/index.json — the per-município air-quality monitoring-station index that powers the My-Area "Качество на въздуха" tile. Covers ~37 município-attributed stations + ~14 background/mountain stations (Витиня, Копитото, Рожен — КФС, Гара Яна) from ИАОС's quarterly publication on data.egov.bg.
Source contract:
The CSVs do not carry station coordinates. Stations are identified by name only (София - Хиподрума, Пловдив - Каменица, В.Търново - РИОСВ, …); scripts/air/build_index.ts parses <município> - <subname> and looks up the município name in data/municipalities.json, with a small alias map for abbreviations (В.Търново → Велико Търново, Г.Оряховица → Горна Оряховица, …). Sofia citywide stations key under SOF00 and fan out to the 24 районы via the useAirQuality hook fallback.
When to run
| Trigger | Action |
|---|
Daily watcher reports ИАОС air quality (data.egov.bg) as changed | Step 1 |
| User asks to "refresh air quality" / "update ИАОС stations" / "new air-quality quarter" | Step 1 |
| Adding NO₂ / O₃ / SO₂ / CO to the per-station record | Step 0 then Step 1 |
Fresh clone with no data/air/index.json or zero stations | Step 1 |
ИАОС publishes once per quarter. A weekly check window catches in-quarter corrections (which do happen).
Step 0 (optional) — Add a new pollutant
ИАОС exposes NO₂, CO, O₃, SO₂ datasets on the same portal but the build script today reads only PM10 + PM2.5. To extend:
-
Open the dataset page for the pollutant in the ИАОС data.egov.bg organisation listing (e.g. data.egov.bg/data?org[0]=76).
-
Click the latest quarterly resource; copy its UUID from the URL data/resourceView/<uuid>.
-
Append the entry to POLLUTANTS at the top of scripts/air/build_index.ts:
const POLLUTANTS = {
pm10: "452acfd4-9fa1-4ab8-9213-f1b2736ce143",
pm25: "0eefa354-495f-4a2d-a40f-846e11dd396a",
no2: "<new-uuid>",
} as const;
-
Add the matching key to the Pollutant type and the pollutants block in data/air/index.json's output (the script already passes these through).
The watcher only tracks PM10 — all ИАОС pollutants publish on the same cadence, so adding NO₂ resources here doesn't need a new watch source.
Step 1 — Build
npx tsx scripts/air/build_index.ts
Force a fresh fetch (skip the disk cache):
rm -rf raw_data/air/iaos
npx tsx scripts/air/build_index.ts
Expected output:
pm10: 47 stations
pm25: 10 stations
Wrote .../data/air/index.json — 37 município stations, 14 background stations, asOf 2026-03-31
Step 2 — Spot-check
python3 -c "
import json
d = json.load(open('data/air/index.json'))
print('snapshotAsOf:', d['snapshotAsOf'])
print('stations:', len(d['stations']))
print('background:', len(d['backgroundStations']))
# Sample a Sofia + a non-Sofia station
for s in d['stations']:
if s['name'].startswith('София'):
print(f' {s[\"name\"]} → {s[\"obshtina\"]} · PM10={s[\"latestReadings\"].get(\"pm10\")}')
break
"
Sanity checks:
snapshotAsOf is the period-end date of the latest CSV (YYYY-03-31, -06-30, -09-30, or -12-31).
- 30–45 município stations (varies as ИАОС adds/retires sites).
- Sofia stations all map to
SOF00.
- No station mapped to an oblast code (
BLG, VAR, …) — only obshtina codes (BLG03, VAR06, …).
If the município-mapped count drops sharply, ИАОС likely renamed a station or added a new town not in the alias map — inspect the script's STATION_PREFIX_ALIASES and BACKGROUND set.
Step 3 — Commit + bucket sync
git add data/air/index.json raw_data/air/iaos/
git commit -m "air: refresh ИАОС stations through <snapshotAsOf>"
npm run bucket:sync:dry
npm run bucket:sync
Step 4 — Stamp success
npx tsx scripts/stamp-ingest.ts update-air-quality \
--summary "37 município stations + 14 background, asOf <snapshotAsOf>"
Append to the public data-changes log only when readings actually changed:
if [ -n "$(git diff --stat data/air/)" ]; then
npx tsx scripts/append-data-change.ts update-air-quality \
--summary "37 município stations + 14 background, asOf <snapshotAsOf>" \
--source "ИАОС air quality (data.egov.bg)"
fi
Known limitations
- No station coordinates in the source. Município attribution is via station-name parsing. A handful of stations whose names break the
<município> - <subname> convention (e.g. "Девня - Изворите" — the "Изворите" suffix is a quarter, not the município) need alias entries; current coverage is ~80% of the active station list.
- Background stations don't bind to a município. They go into a separate
backgroundStations list and the tile renders them only on a future "regional context" expansion.
- PM10 + PM2.5 only. NO₂ / CO / O₃ / SO₂ datasets exist on data.egov.bg but their per-resource UUIDs need discovery. See Step 0 above.
What this skill does NOT do
- Does not normalise to EU thresholds in the data. Thresholds (
euLimit) are part of the pollutants metadata in the output; the tile colour-codes against them client-side.
File map
| Path | Purpose |
|---|
scripts/air/build_index.ts | CLI entry — fetch, parse, map → obshtina, write |
raw_data/air/iaos/{pm10,pm25}.csv | cached source CSVs |
data/air/index.json | output — stations[] + backgroundStations[] |
scripts/watch/sources/iaos_air_quality.ts | watcher — fingerprints PM10 dataset resource UUIDs |
src/data/air/useAirQuality.tsx | React Query hook with Sofia район → SOF00 fallback |
src/screens/myarea/MyAreaAirTile.tsx | "Качество на въздуха" tile |