Readme

easyAQI

Get the Air Quality Index (AQI) for a location via open data sources.

It's "easy" because it strings together multiple lower-level APIs to give you a simple interface for AQI.

  1. Accepts a location in basically any string format (ie "downtown manhattan")
  2. Uses Nominatim to turn that into longitude and latitude
  3. Finds the closest sensor to you on OpenAQ
  4. Pulls the readings from OpenAQ
  5. Calculates the AQI via EPA's NowCAST algorithm
  6. Uses EPA's ranking to classify the severity of the score (ie "Unhealthy for Sensitive Groups")

It uses blob storage to cache the openai location id for your location string to skip a couple steps for the next time.

Example usage

@stevekrouse.easyAQI({ location: "brooklyn navy yard" }) // Returns { "aqi": 23.6, "severity": "Good" }

Forkable example: val.town/v/stevekrouse.easyAQIExample

Also useful for getting alerts when the AQI is unhealthy near you: https://www.val.town/v/stevekrouse.aqi

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { blob } from "https://esm.town/v/std/blob?v=12";
import { nominatimSearch } from "https://esm.town/v/stevekrouse/nominatimSearch";
import { nowcastPMAQISeverity } from "https://esm.town/v/stevekrouse/nowcastPMAQISeverity";
import { openAQLocation as findOpenAQLocation } from "https://esm.town/v/stevekrouse/openAQLocation";
import { openAqNowcastAQI } from "https://esm.town/v/stevekrouse/openAqNowcastAQI";
const cacheKey = location => "easyAQI_locationID_cache_" + encodeURIComponent(location);
export async function easyAQI({ location }: {
location: string;
}) {
let openAQLocation = await blob.getJSON(cacheKey(location));
if (!openAQLocation) {
const [geo] = await nominatimSearch({
q: location,
});
console.log("Searching for openAQ stations near: " + geo.display_name);
openAQLocation = await findOpenAQLocation({
lat: parseFloat(geo.lat),
lon: parseFloat(geo.lon),
});
await blob.setJSON(cacheKey(location), { name: openAQLocation.name, id: openAQLocation.id });
}
console.log("Pulling pm2.5 from station: " + openAQLocation.name);
const aqi = await openAqNowcastAQI({
location_id: openAQLocation.id,
});
return { aqi, severity: nowcastPMAQISeverity(aqi) };
}
Val Town is a social website to write and deploy JavaScript.
Build APIs and schedule functions from your browser.
Comments
5
midnightlightning avatar

Doing a triple-lookup for every run ("human-friendly" location name to lat/lng, lat/lng to list of stations, individual station data) is a decent amount of overhead, and more possibilities for an individual request to fail.

I'd recommend using val.town's SQLite feature to create a table matching LOCATION values to "openAQLocation IDs". Thus, if the location never changes for a user (likely), the first two queries can be skipped most runs.

stevekrouse avatar

Great idea! What do you think of this implementation? https://www.val.town/pulls/bf6db21e-1d03-11ef-a060-cab58f78796a

midnightlightning avatar

That looks wonderful! Since your example "location" value has spaces in it, likely most users will follow suit. That means your blob cache key will have underscores for the first part of the key, and then spaces for the last half. That's not too big a deal, but you may want to do some light sanitizing on the user's input to make it consistent (all lower-case, all whitespaces to underscores, all non-alphanumeric characters removed) and a little more human-friendly if a user uses the "admin" tools to browse their blob objects.

stevekrouse avatar

great idea!

stevekrouse avatar

done and merged :)

v5
May 30, 2024