Air Quality (CN)
Current AQI for your city, with the colour and icon of its AQI band — green at 50 and below, shading to purple past 300.
| System | AWTRIX NG Scripts |
|---|---|
| Firmware | AWTRIX NG |
| Topic | Smarthome |
| Built by | Golevka2001 |
| File | 7ruuDQPaRgdo.ax · 4.3 KB |
| Icons | 7 |
| Published | 30 Aug 2026 |
Current AQI for your city, with the colour and icon of its AQI band — green at 50 and below, shading to purple past 300.
Press the select button for an instant refresh.
Setup: get a key at https://tianapi.com — register, activate the 空气质量指数 (air quality index) interface in your console, then copy your API key into settings. Enter the city name in Chinese (南京, 上海).
Heads-up: the Tianapi AQI interface covers mainland China only.
The six band icons are fixed IDs (47651–47657), not configurable, and must be installed on your device.
7ruuDQPaRgdo.ax
# @name Air Quality (CN)
# @desc AQI for a Chinese city from the Tianapi air quality API
# @author Golevka2001
# @version 1.0
# @config api_key text "API Key" help="tianapi.com console"
# @config area text "City" default="北京" help="Chinese city name"
# @config every number "Refresh" default=60 min=1 unit=min
class AirQuality
# AQI band thresholds with the matching icon and colour (China's standard)
var levels, url, aqi, label, ic, color, tx, err
# refresh countdown + failure backoff, see due()/failed()/ok()/now()
var ticks, span, retry, busy
def init()
self.levels = [
[50, "47651", 0x71D608],
[100, "47652", 0xFAFF1B],
[150, "47653", 0xF98718],
[200, "47654", 0xF70017],
[300, "47655", 0x8F00FF],
[-1, "47656", 0x870089],
]
# saving a setting restarts the app, so the URL built here never goes stale
self.url = ""
var key = store.get("api_key")
if key != nil && key != ""
self.url = "https://apis.tianapi.com/aqi/index?key=" + str(key) +
"&area=" + str(store.get("area"))
end
self.aqi = nil
self.label = "--"
self.ic = "47657"
self.color = 0x666666
self.tx = 9
self.ticks = 0
self.span = 60
self.retry = 30
self.busy = false
self.err = false
end
# first fetch + fill display state before the first frame
def setup()
self.loop()
end
# select forces a refresh; left and right just rotate
def on_button(btn)
if btn == "select"
self.now()
end
end
def on_body(body, status)
shared.set("f", 0)
# non-200 or nil body: nothing usable this attempt
if status != 200 || body == nil
self.failed()
return
end
var m = re.search("\"aqi\":\"?(\\d+)", body)
if m == nil
self.failed()
return
end
var n = num(m[1])
if n == nil
self.failed()
return
end
self.ok()
self.aqi = n
end
def loop()
# url checked first: due() raises busy as a side effect
if self.url != "" && self.due(store.get("every"))
shared.set("f", 1)
# keep only the 24 bytes after the needle
http.get(self.url, / b, st -> self.on_body(b, st),
{'find': "\"aqi\":", 'keep': 24})
end
# pick the band, colour and label for the current reading
if self.aqi != nil
self.label = str(int(self.aqi))
var i = 0
while i < size(self.levels) - 1 && self.levels[i][0] >= 0 &&
self.aqi > self.levels[i][0]
i += 1
end
self.ic = self.levels[i][1]
self.color = self.levels[i][2]
else
self.label = self.err ? "?" : "--"
self.color = self.err ? 0xCC7722 : 0x666666
end
self.tx = 9 + int((width() - 9 - text_ink_width(self.label)) / 2)
end
# Fires at most once per interval, never while a request is out or another
# app's TLS handshake is running (shared "f" flag, official convention).
# Raises busy, so only call it when a request will actually be issued.
def due(every)
self.span = max(60, num(every, 1) * 60)
if self.ticks > 0
self.ticks = self.ticks - 1
return false
end
if self.busy
return false
end
for k : shared.keys()
var n = size(k)
if n > 2 && k[n - 2 .. n - 1] == ".f" && shared.get(k) == 1
var a = shared.age(k)
if a != nil && a < 20000
return false
end
end
end
self.ticks = self.span
self.busy = true
return true
end
# failure backoff: retry at 30s, doubling, capped at the interval; raises
# err so a fetch with no data yet can show "?" instead of "..."
def failed()
self.busy = false
self.ticks = self.retry
self.retry = min(self.retry * 2, self.span)
self.err = true
end
def ok()
self.busy = false
self.retry = 30
self.err = false
end
# manual refresh: clears the countdown but leaves busy alone, so a press
# while a request is out can't fire a second one
def now()
self.ticks = 0
self.retry = 30
end
def draw()
clear()
if !icon(self.ic, 0, 0)
rect_fill(0, 0, 8, 8, 0x222222)
end
text(self.tx, 6, self.label, self.color)
end
end
return AirQuality()
Install it on your AWTRIX NG
Your browser writes the script straight to the device on your network. It joins the app rotation right away.
Some browsers refuse to talk to a device on your network from a public page. Download the flow and paste it into the Scripts tab of your device, or install it from a terminal:
These icons belong on the device, in /ICONS. Each one is at
most 32×8
pixels — shown here magnified, at their real proportions.
8×8 px
8×8 px
8×8 px
8×8 px
8×8 px
8×8 px
8×8 px
More flows for AWTRIX NG Scripts or Smarthome
Something wrong with this flow? Report it.