From dee50186592172f6e98f28ea0798e84d4e82fcd7 Mon Sep 17 00:00:00 2001 From: David Holdeman Date: Fri, 17 Oct 2025 14:59:50 +0800 Subject: [PATCH 1/3] Create iqair clockinfo make lint happier, standardize indentation --- apps/airqualityci/ChangeLog | 1 + apps/airqualityci/README.md | 19 ++++++ apps/airqualityci/app.png | Bin 0 -> 1664 bytes apps/airqualityci/clkinfo.js | 82 ++++++++++++++++++++++ apps/airqualityci/custom.html | 116 ++++++++++++++++++++++++++++++++ apps/airqualityci/metadata.json | 18 +++++ 6 files changed, 236 insertions(+) create mode 100644 apps/airqualityci/ChangeLog create mode 100644 apps/airqualityci/README.md create mode 100644 apps/airqualityci/app.png create mode 100644 apps/airqualityci/clkinfo.js create mode 100644 apps/airqualityci/custom.html create mode 100644 apps/airqualityci/metadata.json diff --git a/apps/airqualityci/ChangeLog b/apps/airqualityci/ChangeLog new file mode 100644 index 0000000000..cd5afdcc99 --- /dev/null +++ b/apps/airqualityci/ChangeLog @@ -0,0 +1 @@ +0.01: Initial clockinfo release diff --git a/apps/airqualityci/README.md b/apps/airqualityci/README.md new file mode 100644 index 0000000000..7a057fa558 --- /dev/null +++ b/apps/airqualityci/README.md @@ -0,0 +1,19 @@ +# Air Quality Clock Info + +Create a list of clockinfos, each displaying air quality and/or temperature, fetched from IQAir + +Requires Gadgetbridge internet access + +# Display modes + +Tapping the clockinfo changes its mode. + +Examples of possible modes: +- `45` +- `45 31°` +- `NYC 45` +- `NYC 45 31°` +- `NYC` + + +Not affiliated with IQAir. diff --git a/apps/airqualityci/app.png b/apps/airqualityci/app.png new file mode 100644 index 0000000000000000000000000000000000000000..3790b7b2d7e58594d6f43cb3cf5e0fc81a8ee34d GIT binary patch literal 1664 zcmV-`27md9P)z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007 zbV*G`2k8nC4L20sk1wkL00qQJL_t(&-pyJ)h$KZ6{=S-?znz`s=4C7P&iT0i_YXi0}qVMM9|1cFc1SVP%cqe+}-J(>6!fue6{hXy_)`37o|q+4#Z zBZ0XAj=2DEWY1^zY+>~>&IBvKj~(dvd*aFw$y?`uyG!yK8j_^j2598~d*Hb%XO2vM zhJ>WO1pL^I{+_E(NPgS^>K@4#&@xH7>t@1_^pWNG`!83|J&DYw+$-i7z^>(~1AnUp zrVHqnE&!O5yk0@>hUr0OkAgD>t7s5{Q^>Isn+SdiV6P>3Si3H1O7g|37uM&V!?7C& z(_{8lXUB<+Z`1|Uv*Hr(O!AeA&VM3#wV}hlq=MwAqWzKe*(Hq)crX+pSQW-svwxR=) zbZtWy5+q+XoCFYp3*s}c%#wcSIGSi8Vc@sn8^t&#WR5`a%#6e@&i5Xi=ihh|RUo$r z9v*Y(Ag+bl$ly0?a%Z@nIDV$DJlB(DtYNuA{7bwA!h zsl_#>O46qhvnLS!?b2=GJ-Y4;G?7>ryrpGIKt4<}@Q)`^)_w;&f1(35O1%kqcu5Iv z`xA%iwm2;|}qX zm$5)y`L_Im=BYAfXGy1I$ma5JaY}3}C z&_^*Y0L)0fTLb>c5ty|;W+i#21@L>xK2z$IJuV*{fI^ zN*Ddbk>Bi~+d!;+)c|MjR?zJ+RbBR2x?p50R}C^}#wPY+hSOUme$?W0$J}_?qhbbD zi6~WF_Q(@51x|0UM8e0>>}IB9GBdX8{vcN_arSCVj{nJZo4)~?GE;H#;J~^70000< KMNUMnLSTZ%X9wy4 literal 0 HcmV?d00001 diff --git a/apps/airqualityci/clkinfo.js b/apps/airqualityci/clkinfo.js new file mode 100644 index 0000000000..952f8a5e12 --- /dev/null +++ b/apps/airqualityci/clkinfo.js @@ -0,0 +1,82 @@ +(function() { + let data = require("Storage").readJSON("airqualityci.json", 1) || {}; + let config = require("Storage").readJSON("airqualityci.settings.json") || {}; + + function checkAQI(cb) { + let d = new Date(); + if (config.apiKey && config.rows && (data.aqiTime === undefined || data.aqiTime + 3600000 < d.getTime())) { + data.aqi = []; + config.rows.forEach((row) => { + if (row.url) { + let el = row.url.replace(/\/$/, "").split("/").reverse(); + let url = `https://api.airvisual.com/v2/city?city=${el[0]}&state=${el[1]}&country=${el[2]}&key=${config.apiKey}`; + Bangle.http(url).then((r) => { + let resp = JSON.parse(r.resp); + data.aqiTime = d.getTime(); + data.aqi.push({ + url: row.url, + aqius: resp.data.current.pollution.aqius, + temp: resp.data.current.weather.tp + }); + cb(); + require("Storage").writeJSON("airqualityci.json", data); + }); + } + }); + } + } + + let locs = { + name: "AirQuality", + items: [] + }; + + if (config.rows) { + config.rows.forEach((row, id) => { + locs.items.push({ + name: "AQI " + row.name, + show: () => {}, + hide: () => {}, + run: function() { + if ( ! config.rows[id].mode ) { + config.rows[id].mode = 2; + } else if ( config.rows[id].mode === 5 ){ + config.rows[id].mode = 1; + } else { + config.rows[id].mode += 1; + } + this.emit("redraw"); + require("Storage").writeJSON("airqualityci.settings.json", config); + }, + get: function() { + checkAQI(() => this.emit("redraw")); + let aqi = data.aqi.find((e) => e.url == config.rows[id].url); + let txt = ""; + switch ( config.rows[id].mode) { + case 2: + txt = ((aqi && aqi.aqius) || "...") + " " + ((aqi && aqi.temp) || "...") + "°"; + break; + case 3: + txt = row.name + " " + ((aqi && aqi.aqius) || "..."); + break; + case 4: + txt = row.name + " " + ((aqi && aqi.aqius) || "...") + " " + ((aqi && aqi.temp) || "...") + "°"; + break; + case 5: + txt = row.name; + break; + default: + txt = ((aqi && aqi.aqius) || "..."); + } + return { + text: txt, + short: ((aqi && aqi.aqius) || "..."), + img: atob("GBiBAAA4AAB8AAd+AA++QB/d8D/t8D/p+DvqcBUPsA7vgB9fwA7vgAXvgAH/AAHwgAD3wAbfwA9/wA+/gA/fAA/OAA/gAAfAAAAAAA==") + }; + } + }); + }); + } + + return locs; +}) diff --git a/apps/airqualityci/custom.html b/apps/airqualityci/custom.html new file mode 100644 index 0000000000..d615d45bcf --- /dev/null +++ b/apps/airqualityci/custom.html @@ -0,0 +1,116 @@ + + + + + + +

The AQI and temp are sourced from IQAir, which requires "Allow Internet Access" to be enabled in Gadgetbridge.

+

To use AQI, you need an IQAir API key. To get one for free, create an IQAir account, then create an API key in your user dashboard.

+

To specify the IQAir source, find your city on the IQAir website, then copy and paste the URL. It should look something like https://www.iqair.com/usa/new-york/new-york-city

+ + + + + +
NameIQAir URL
+ + + +
+ +
+ + + + + + + + diff --git a/apps/airqualityci/metadata.json b/apps/airqualityci/metadata.json new file mode 100644 index 0000000000..e43967dcfa --- /dev/null +++ b/apps/airqualityci/metadata.json @@ -0,0 +1,18 @@ +{ "id": "airqualityci", + "name": "Air Quality Clockinfo", + "version": "0.01", + "description": "Create clockinfos for air quality and temperature", + "icon": "app.png", + "type": "clkinfo", + "tags": "clkinfo", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "custom": "custom.html", + "storage": [ + {"name":"airqualityci.clkinfo.js","url":"clkinfo.js"} + ], + "data": [ + {"name":"airqualityci.json"}, + {"name":"airqualityci.settings.json"} + ] +} From 1bd10df92b1c84f0178cd4058fc3e8b57adadc9a Mon Sep 17 00:00:00 2001 From: David Holdeman Date: Thu, 23 Oct 2025 18:25:22 +0800 Subject: [PATCH 2/3] Refactor data structure, change update conditions --- apps/airqualityci/clkinfo.js | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/apps/airqualityci/clkinfo.js b/apps/airqualityci/clkinfo.js index 952f8a5e12..428dd77b5e 100644 --- a/apps/airqualityci/clkinfo.js +++ b/apps/airqualityci/clkinfo.js @@ -4,23 +4,30 @@ function checkAQI(cb) { let d = new Date(); - if (config.apiKey && config.rows && (data.aqiTime === undefined || data.aqiTime + 3600000 < d.getTime())) { - data.aqi = []; + if (config.apiKey && config.rows) { config.rows.forEach((row) => { if (row.url) { let el = row.url.replace(/\/$/, "").split("/").reverse(); let url = `https://api.airvisual.com/v2/city?city=${el[0]}&state=${el[1]}&country=${el[2]}&key=${config.apiKey}`; - Bangle.http(url).then((r) => { - let resp = JSON.parse(r.resp); - data.aqiTime = d.getTime(); - data.aqi.push({ - url: row.url, - aqius: resp.data.current.pollution.aqius, - temp: resp.data.current.weather.tp + data[row.url] = data[row.url] || {}; + // If neither attempt nor time are set, then we have never tried + // If attempt was set more than 1 minute ago, try again + // If time was set more than 1 hour ago, refresh + if ((!data[row.url].time && !data[row.url].attempt) || + (data[row.url].attempt && data[row.url].attempt + 60000 < d.getTime()) || + (data[row.url].time && data[row.url].time + 3600000 < d.getTime())) { + data[row.url].attempt = d.getTime(); + Bangle.http(url).then((r) => { + let resp = JSON.parse(r.resp); + data[row.url] = { + aqius: resp.data.current.pollution.aqius, + temp: resp.data.current.weather.tp, + time: d.getTime() + } + cb(); + require("Storage").writeJSON("airqualityci.json", data); }); - cb(); - require("Storage").writeJSON("airqualityci.json", data); - }); + } } }); } @@ -50,7 +57,7 @@ }, get: function() { checkAQI(() => this.emit("redraw")); - let aqi = data.aqi.find((e) => e.url == config.rows[id].url); + let aqi = data[config.rows[id].url] let txt = ""; switch ( config.rows[id].mode) { case 2: From 4f36b6c93e603c26a3bef1921988182511bd4534 Mon Sep 17 00:00:00 2001 From: David Holdeman Date: Fri, 31 Oct 2025 19:26:10 +0800 Subject: [PATCH 3/3] oops, zero is falsy --- apps/airqualityci/clkinfo.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/airqualityci/clkinfo.js b/apps/airqualityci/clkinfo.js index 428dd77b5e..c5730b5457 100644 --- a/apps/airqualityci/clkinfo.js +++ b/apps/airqualityci/clkinfo.js @@ -59,25 +59,27 @@ checkAQI(() => this.emit("redraw")); let aqi = data[config.rows[id].url] let txt = ""; + let aqius = (aqi && typeof(aqi.aqius) == "number") ? aqi.aqius : "..."; + let temp = (aqi && typeof(aqi.temp) == "number") ? aqi.temp : "..."; switch ( config.rows[id].mode) { case 2: - txt = ((aqi && aqi.aqius) || "...") + " " + ((aqi && aqi.temp) || "...") + "°"; + txt = aqius + " " + temp + "°"; break; case 3: - txt = row.name + " " + ((aqi && aqi.aqius) || "..."); + txt = row.name + " " + aqius; break; case 4: - txt = row.name + " " + ((aqi && aqi.aqius) || "...") + " " + ((aqi && aqi.temp) || "...") + "°"; + txt = row.name + " " + aqius + " " + temp + "°"; break; case 5: txt = row.name; break; default: - txt = ((aqi && aqi.aqius) || "..."); + txt = aqius; } return { text: txt, - short: ((aqi && aqi.aqius) || "..."), + short: aqius, img: atob("GBiBAAA4AAB8AAd+AA++QB/d8D/t8D/p+DvqcBUPsA7vgB9fwA7vgAXvgAH/AAHwgAD3wAbfwA9/wA+/gA/fAA/OAA/gAAfAAAAAAA==") }; }