From a809b128bc61a6000135ece04a4be8d8d1a98b80 Mon Sep 17 00:00:00 2001 From: Flaparoo <629229+flaparoo@users.noreply.github.com> Date: Tue, 28 Nov 2023 17:46:29 +0800 Subject: [PATCH] New app: aviatorclk (and module avwx) --- apps/aviatorclk/.gitignore | 1 + apps/aviatorclk/ChangeLog | 1 + apps/aviatorclk/README.md | 36 ++++ apps/aviatorclk/aviatorclk-icon.js | 1 + apps/aviatorclk/aviatorclk.app.js | 283 +++++++++++++++++++++++++ apps/aviatorclk/aviatorclk.png | Bin 0 -> 2895 bytes apps/aviatorclk/aviatorclk.settings.js | 35 +++ apps/aviatorclk/metadata.json | 20 ++ apps/aviatorclk/screenshot.png | Bin 0 -> 4352 bytes apps/aviatorclk/screenshot2.png | Bin 0 -> 4427 bytes apps/avwx/ChangeLog | 1 + apps/avwx/README.md | 41 ++++ apps/avwx/avwx.js | 47 ++++ apps/avwx/avwx.png | Bin 0 -> 1809 bytes apps/avwx/interface.html | 47 ++++ apps/avwx/metadata.json | 18 ++ 16 files changed, 531 insertions(+) create mode 100644 apps/aviatorclk/.gitignore create mode 100644 apps/aviatorclk/ChangeLog create mode 100644 apps/aviatorclk/README.md create mode 100644 apps/aviatorclk/aviatorclk-icon.js create mode 100644 apps/aviatorclk/aviatorclk.app.js create mode 100644 apps/aviatorclk/aviatorclk.png create mode 100644 apps/aviatorclk/aviatorclk.settings.js create mode 100644 apps/aviatorclk/metadata.json create mode 100644 apps/aviatorclk/screenshot.png create mode 100644 apps/aviatorclk/screenshot2.png create mode 100644 apps/avwx/ChangeLog create mode 100644 apps/avwx/README.md create mode 100644 apps/avwx/avwx.js create mode 100644 apps/avwx/avwx.png create mode 100644 apps/avwx/interface.html create mode 100644 apps/avwx/metadata.json diff --git a/apps/aviatorclk/.gitignore b/apps/aviatorclk/.gitignore new file mode 100644 index 000000000..bdbc0d22e --- /dev/null +++ b/apps/aviatorclk/.gitignore @@ -0,0 +1 @@ +aviatorclk.json diff --git a/apps/aviatorclk/ChangeLog b/apps/aviatorclk/ChangeLog new file mode 100644 index 000000000..971e5b97e --- /dev/null +++ b/apps/aviatorclk/ChangeLog @@ -0,0 +1 @@ +1.00: initial release diff --git a/apps/aviatorclk/README.md b/apps/aviatorclk/README.md new file mode 100644 index 000000000..fe7376b5d --- /dev/null +++ b/apps/aviatorclk/README.md @@ -0,0 +1,36 @@ +# Aviator Clock + +A clock for aviators, with local time and UTC - and the latest METAR +(Meteorological Aerodrome Report) for the nearest airport + +![](screenshot.png) +![](screenshot2.png) + +This app depends on the [AVWX module](?id=avwx). Make sure to configure that +module after installing this app. + + +## Features + +- Local time (with optional seconds) +- UTC / Zulu time +- Weekday and day of the month +- Latest METAR for the nearest airport (scrollable) + +Tap the screen in the top or bottom half to scroll the METAR text (in case not +the whole report fits on the screen). + +The colour of the METAR text will change to orange if the report is more than +1h old, and red if it's older than 1.5h. + + +## Settings + +- **Show Seconds**: to conserve battery power, you can turn the seconds display off +- **Invert Scrolling**: swaps the METAR scrolling direction of the top and bottom taps + + +## Author + +Flaparoo [github](https://github.com/flaparoo) + diff --git a/apps/aviatorclk/aviatorclk-icon.js b/apps/aviatorclk/aviatorclk-icon.js new file mode 100644 index 000000000..508769a66 --- /dev/null +++ b/apps/aviatorclk/aviatorclk-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwg96iIACCqMBCwYABiAWQiUiAAUhDBwWGDCAWHDAYuMCw4ABGBYWKGBYuLGBcBLpAXNFxhIKFxgwCIyhIJC58hC44WNC5B2NPBIXbBYIAHNgIXKCpAYEC5AhBII8SDAQXJMI5EEC6ZREC6EhFwkRO4zuCC46AFAgLYEC4YCBIoaADF4gXEKgYXDVBAcCXxBZDkcyDRAXHmILCif//4GEC5f/PQQWB//zbAX/C5gAKC78BC6K/In4WJ+YXW+QXHMAURl4XJeQYWEGALhBC4q+BYYLbDFwowCkLTCRIyNHGArNBC48SFxIXCMApHDOwQXIJAIQCAAaWCDYJGIDAipGFwQWKDAUSDAnzUoIWMDAcjn/zUgQWOPYYADOZJjKFqIAp")) diff --git a/apps/aviatorclk/aviatorclk.app.js b/apps/aviatorclk/aviatorclk.app.js new file mode 100644 index 000000000..1d99fdbde --- /dev/null +++ b/apps/aviatorclk/aviatorclk.app.js @@ -0,0 +1,283 @@ +/* + * Aviator Clock - Bangle.js + * + */ + +const COLOUR_DARK_GREY = 0x4208; // same as: g.setColor(0.25, 0.25, 0.25) +const COLOUR_GREY = 0x8410; // same as: g.setColor(0.5, 0.5, 0.5) +const COLOUR_LIGHT_GREY = 0xc618; // same as: g.setColor(0.75, 0.75, 0.75) +const COLOUR_RED = 0xf800; // same as: g.setColor(1, 0, 0) +const COLOUR_BLUE = 0x001f; // same as: g.setColor(0, 0, 1) +const COLOUR_YELLOW = 0xffe0; // same as: g.setColor(1, 1, 0) +const COLOUR_LIGHT_CYAN = 0x87ff; // same as: g.setColor(0.5, 1, 1) +const COLOUR_DARK_YELLOW = 0x8400; // same as: g.setColor(0.5, 0.5, 0) +const COLOUR_DARK_CYAN = 0x0410; // same as: g.setColor(0, 0.5, 0.5) +const COLOUR_ORANGE = 0xfc00; // same as: g.setColor(1, 0.5, 0) + +const APP_NAME = 'aviatorclk'; + +const horizontalCenter = g.getWidth()/2; +const mainTimeHeight = 38; +const secondaryFontHeight = 22; +const dateColour = ( g.theme.dark ? COLOUR_YELLOW : COLOUR_BLUE ); +const UTCColour = ( g.theme.dark ? COLOUR_LIGHT_CYAN : COLOUR_DARK_CYAN ); +const separatorColour = ( g.theme.dark ? COLOUR_LIGHT_GREY : COLOUR_DARK_GREY ); + +const avwx = require('avwx'); + + +// read in the settings +var settings = Object.assign({ + showSeconds: true, + invertScrolling: false, +}, require('Storage').readJSON(APP_NAME+'.json', true) || {}); + + +// globals +var drawTimeout; +var secondsInterval; +var avwxTimeout; + +var AVWXrequest; +var METAR = ''; +var METARlinesCount = 0; +var METARscollLines = 0; +var METARts; + + + +// date object to time string in format HH:MM[:SS] +// (with a leading 0 for hours if required, unlike the "locale" time() function) +function timeStr(date, seconds) { + let timeStr = date.getHours().toString(); + if (timeStr.length == 1) timeStr = '0' + timeStr; + let minutes = date.getMinutes().toString(); + if (minutes.length == 1) minutes = '0' + minutes; + timeStr += ':' + minutes; + if (seconds) { + let seconds = date.getSeconds().toString(); + if (seconds.length == 1) seconds = '0' + seconds; + timeStr += ':' + seconds; + } + return timeStr; +} + + +// draw the METAR info +function drawAVWX() { + let now = new Date(); + let METARage = 0; // in minutes + if (METARts) { + METARage = Math.floor((now - METARts) / 60000); + } + + g.setBgColor(g.theme.bg); + + let y = Bangle.appRect.y + mainTimeHeight + secondaryFontHeight + 4; + g.clearRect(0, y, g.getWidth(), y + (secondaryFontHeight * 4)); + + g.setFontAlign(0, -1).setFont("Vector", secondaryFontHeight); + if (METARage > 90) { // older than 1.5h + g.setColor(COLOUR_RED); + } else if (METARage > 60) { // older than 1h + g.setColor( g.theme.dark ? COLOUR_ORANGE : COLOUR_DARK_YELLOW ); + } else { + g.setColor(g.theme.fg); + } + let METARlines = g.wrapString(METAR, g.getWidth()); + METARlinesCount = METARlines.length; + METARlines.splice(0, METARscollLines); + g.drawString(METARlines.join("\n"), horizontalCenter, y, true); + + if (! avwxTimeout) { avwxTimeout = setTimeout(updateAVWX, 5 * 60000); } +} + +// update the METAR info +function updateAVWX() { + if (avwxTimeout) clearTimeout(avwxTimeout); + avwxTimeout = undefined; + + METAR = '\nGetting GPS fix'; + METARlinesCount = 0; METARscollLines = 0; + METARts = undefined; + drawAVWX(); + + Bangle.setGPSPower(true, APP_NAME); + Bangle.on('GPS', fix => { + // prevent multiple, simultaneous requests + if (AVWXrequest) { return; } + + if ('fix' in fix && fix.fix != 0 && fix.satellites >= 4) { + Bangle.setGPSPower(false, APP_NAME); + let lat = fix.lat; + let lon = fix.lon; + + METAR = '\nRequesting METAR'; + METARlinesCount = 0; METARscollLines = 0; + METARts = undefined; + drawAVWX(); + + // get latest METAR from nearest airport (via AVWX API) + AVWXrequest = avwx.request('metar/'+lat+','+lon, 'onfail=nearest', data => { + if (avwxTimeout) clearTimeout(avwxTimeout); + avwxTimeout = undefined; + + let METARjson = JSON.parse(data.resp); + + if ('sanitized' in METARjson) { + METAR = METARjson.sanitized; + } else { + METAR = 'No "sanitized" METAR data found!'; + } + METARlinesCount = 0; METARscollLines = 0; + + if ('time' in METARjson) { + METARts = new Date(METARjson.time.dt); + let now = new Date(); + let METARage = Math.floor((now - METARts) / 60000); // in minutes + if (METARage <= 30) { + // some METARs update every 30 min -> attempt to update after METAR is 35min old + avwxTimeout = setTimeout(updateAVWX, (35 - METARage) * 60000); + } else if (METARage <= 60) { + // otherwise, attempt METAR update after it's 65min old + avwxTimeout = setTimeout(updateAVWX, (65 - METARage) * 60000); + } + } else { + METARts = undefined; + } + + drawAVWX(); + AVWXrequest = undefined; + + }, error => { + // AVWX API request failed + console.log(error); + METAR = 'ERR: ' + error; + METARlinesCount = 0; METARscollLines = 0; + METARts = undefined; + drawAVWX(); + AVWXrequest = undefined; + }); + } + }); +} + + +// draw only the seconds part of the main clock +function drawSeconds() { + let now = new Date(); + let seconds = now.getSeconds().toString(); + if (seconds.length == 1) seconds = '0' + seconds; + let y = Bangle.appRect.y + mainTimeHeight - 3; + g.setFontAlign(-1, 1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_GREY); + g.drawString(seconds, horizontalCenter + 54, y, true); +} + +// sync seconds update +function syncSecondsUpdate() { + drawSeconds(); + setTimeout(function() { + drawSeconds(); + secondsInterval = setInterval(drawSeconds, 1000); + }, 1000 - (Date.now() % 1000)); +} + +// set timeout for per-minute updates +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + if (METARts) { + let now = new Date(); + let METARage = Math.floor((now - METARts) / 60000); + if (METARage > 60) { + // the METAR colour might have to be updated: + drawAVWX(); + } + } + draw(); + }, 60000 - (Date.now() % 60000)); +} + +// draw top part of clock (main time, date and UTC) +function draw() { + let now = new Date(); + let nowUTC = new Date(now + (now.getTimezoneOffset() * 1000 * 60)); + + // prepare main clock area + let y = Bangle.appRect.y; + + g.setBgColor(g.theme.bg); + + // main time display + g.setFontAlign(0, -1).setFont("Vector", mainTimeHeight).setColor(g.theme.fg); + g.drawString(timeStr(now, false), horizontalCenter, y, true); + + // prepare second line (UTC and date) + y += mainTimeHeight; + g.clearRect(0, y, g.getWidth(), y + secondaryFontHeight - 1); + + // weekday and day of the month + g.setFontAlign(-1, -1).setFont("Vector", secondaryFontHeight).setColor(dateColour); + g.drawString(require("locale").dow(now, 1).toUpperCase() + ' ' + now.getDate(), 0, y, false); + + // UTC + g.setFontAlign(1, -1).setFont("Vector", secondaryFontHeight).setColor(UTCColour); + g.drawString(timeStr(nowUTC, false) + "Z", g.getWidth(), y, false); + + queueDraw(); +} + + +// initialise +g.clear(true); + +// scroll METAR lines on taps +Bangle.setUI("clockupdown", action => { + switch (action) { + case -1: // top tap + if (settings.invertScrolling) { + if (METARscollLines > 0) + METARscollLines--; + } else { + if (METARscollLines < METARlinesCount - 4) + METARscollLines++; + } + break; + case 1: // bottom tap + if (settings.invertScrolling) { + if (METARscollLines < METARlinesCount - 4) + METARscollLines++; + } else { + if (METARscollLines > 0) + METARscollLines--; + } + break; + default: + // ignore + } + drawAVWX(); +}); + +// load widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +// draw static separator line +y = Bangle.appRect.y + mainTimeHeight + secondaryFontHeight; +g.setColor(separatorColour); +g.drawLine(0, y, g.getWidth(), y); + +// draw times and request METAR +draw(); +if (settings.showSeconds) + syncSecondsUpdate(); +updateAVWX(); + + +// TMP for debugging: +//METAR = 'YAAA 011100Z 21014KT CAVOK 23/08 Q1018 RMK RF000/0000'; +//METAR = 'YAAA 150900Z 14012KT 9999 SCT045 BKN064 26/14 Q1012 RMK RF000/0000 DL-W/DL-NW'; +//METAR = 'YAAA 020030Z VRB CAVOK'; +//METARts = new Date(Date.now() - 61 * 60000); // 61 to trigger warning, 91 to trigger alert + diff --git a/apps/aviatorclk/aviatorclk.png b/apps/aviatorclk/aviatorclk.png new file mode 100644 index 0000000000000000000000000000000000000000..af88cfbc4b3be91ccb7b4b7d0f370224bb52bb9e GIT binary patch literal 2895 zcmV-V3$XNwP)+SYnb=AXHhJ5=u)6 zA%3fKJu|hl;IZQXs<_gTfBJpid%pLcd#9x2%k_V<3|7}qbyi14Cphod39dsr!TFxX z;ykCeIL@hf`#)5ia8SWn@2lm6l-OYXamg29b#``s&0zQVj1KQUqr(e>-2=VN4IP!> zg4XJU#^Qk5;(*F*gOU?aYZ9O^S!d-2{!bDe=i)f-tDlQ_o#57TPVZkhrw=Bl54<~o zSUQ7_u5Jtr4P)!r7`AQOhVk)nY#ALzZ~p+A+uGraB%tB#kQ;c&^gJY5Zbqu-<)l)`C$)4ndpg$No?P~ofQcm9v()2e?NMAd(qR=!>X^Z4}*h)7#d#S)rNf%z5e@GO}yD1 zI7;Fj!5B6Vk6?Ownu#a3+uPfjC=!1mz=h%)8XAyJr&$qPV^a&1oE0@{1FDqz!z&I! zZNs~R2g!ARJc+Sw+n9K2m9DNXw6(P{@p*v7+|P?orBbM`uVTT8%Xc@4cZOmZpP0ao9Xqgf>sEAhbQGMwocnozOSzxQ0VI=2R?%1-60Hf9N*yZX znq4a*-stdat=

e;6ZMMlTY7q5D*y=6)VvzWd1>KwVuOe1Q#T(P^*Mm~ z?h^o&)#+vcrBdY|mnEKe_g{I$ z)cc>7JjJ+ARgC-5Xq1)N=3oG~NECN38G!c$Un79wEu(C{xs?0G99+wFKN^j~V+DYgY!=aEJ-p!<1Wy2*!wbF5b*YgVa-*d%b<)r# z)^cp-5owH&>P(R7IVep$j8;1Ymj|J6xF9|fi69UR%>jrMr1NX&<=dG8~_ji^*@ck z$#f&Tuu$29aRibXL>jY*H+LZ3(t%88AF@3|Xzm_BW>Y_6&F%0eGjPWn;EbhUk0fCU z#=-kzFnJ@e1`}{ZlHmOj=FP&o?`E_xfPTcY8{w~Sf+s=Z)38VCA%yB+3B+Oc zMd66n!&{euJCTMnnuft0hTQB#jfP_Y5{-e04+cXd9&2l4=a&p%cKgp00Odac(5;JF zr)g6^tA@^A2GGzCQld&iwiQd9b$uxavxr#=I%kX4~!JtPquabu;&+oE>7lzyK&ZNqjIE#M&A;)>cc;-y|yXFylDx8qVpTA@@Ux zRAC45A1?Jgxu5MFM91J3wD->eBwM=>OlL3YzBQD9E0IPZ)lzVuc0(n2a_&1IHri07 zH8b&)JOn^hqtww3U`?guz?wB{zP>ns%j2Vumu7#OJn6al)HuD7a7Uq!zpj-8Tp*Q)m|e&<=QgK9ot7 zxT#8tRplanaR8_9337kqrXJ=#4RCaD&3`HhrJD=xH+1wMl4(b%A{4vN4mW!fO ztTlo6&83gV?!Ub;^DcKQuIelZRi#iJ2ELG}y;-pRS7pq%b?6;GImo$Y4Q5 zt-kQ_<_@lccq)g>JqJ)Bdb=2aPx2pc-hBp;bN|^VABM%_UvO~EKXnZ@7kWtX?$ZzwxzW~7N^kJ&AXV!N09A4obN`02>gkeVmy&b-P%F63Q1V2QX*4&tz!QuxfL+s*%Xk0e z<=x*tKEm4P=4vUlCLUg|kAbj{x4h=`_3I@|doQi!1+CWVJVzfc@AM&_OtIV##1i=A z#2ZDs|Lo%r!X&r~xzD>ic+9)Uf?2V;0%X^{I z2tu{mV*fk!ek5K8oz=;1ThBf9aMA8_Hh1CRVuzMYRhT%}=f;M7bdvZDl3Eyf>s;uO{S;D)m5K*T^%bPoCMEsw5BHTqD#^+Xa${A%|Oy!`YZ@Xmo}@Zmd$ zSiODV8N9UjG5liZB)p+8)=TDQ{ back(), + 'Show Seconds': { + value: !!settings.showSeconds, // !! converts undefined to false + format: v => v ? "On" : "Off", + onchange: v => { + settings.showSeconds = v; + writeSettings(); + } + }, + 'Invert Scrolling': { + value: !!settings.invertScrolling, // !! converts undefined to false + format: v => v ? "On" : "Off", + onchange: v => { + settings.invertScrolling = v; + writeSettings(); + } + }, + }); +}) diff --git a/apps/aviatorclk/metadata.json b/apps/aviatorclk/metadata.json new file mode 100644 index 000000000..6ae8c4a18 --- /dev/null +++ b/apps/aviatorclk/metadata.json @@ -0,0 +1,20 @@ +{ + "id": "aviatorclk", + "name": "Aviator Clock", + "shortName":"AV8R Clock", + "version":"1.00", + "description": "A clock for aviators, with local time and UTC - and the latest METAR for the nearest airport", + "icon": "aviatorclk.png", + "screenshots": [{ "url": "screenshot.png" }, { "url": "screenshot2.png" }], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "dependencies" : { "avwx": "module" }, + "readme": "README.md", + "storage": [ + { "name":"aviatorclk.app.js", "url":"aviatorclk.app.js" }, + { "name":"aviatorclk.settings.js", "url":"aviatorclk.settings.js" }, + { "name":"aviatorclk.img", "url":"aviatorclk-icon.js", "evaluate":true } + ], + "data": [{ "name":"aviatorclk.json" }] +} diff --git a/apps/aviatorclk/screenshot.png b/apps/aviatorclk/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..127946f423a4fcdff349cf64a23045e72acbdaa5 GIT binary patch literal 4352 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 literal 0 HcmV?d00001 diff --git a/apps/aviatorclk/screenshot2.png b/apps/aviatorclk/screenshot2.png new file mode 100644 index 0000000000000000000000000000000000000000..e00e2238baa0c0c92c4764ebe793064bf304bd0c GIT binary patch literal 4427 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 literal 0 HcmV?d00001 diff --git a/apps/avwx/ChangeLog b/apps/avwx/ChangeLog new file mode 100644 index 000000000..971e5b97e --- /dev/null +++ b/apps/avwx/ChangeLog @@ -0,0 +1 @@ +1.00: initial release diff --git a/apps/avwx/README.md b/apps/avwx/README.md new file mode 100644 index 000000000..a954d118f --- /dev/null +++ b/apps/avwx/README.md @@ -0,0 +1,41 @@ +# AVWX Module + +This is a module/library to use the [AVWX](https://account.avwx.rest/) Aviation +Weather API. It doesn't include an app. + + +## Configuration + +You will need an AVWX account (see above for link) and generate an API token. +The free "Hobby" plan is normally sufficient, but please consider supporting +the AVWX project. + +After installing the module on your Bangle, use the "interface" page (floppy +disk icon) in the App Loader to set the API token. + + +## Usage + +Include the module in your app with: + + const avwx = require('avwx'); + +Then use the exported function, for example to get the "sanitized" METAR from +the nearest station to a lat/lon coordinate pair: + + reqID = avwx.request('metar/'+lat+','+lon, + 'filter=sanitized&onfail=nearest', + data => { console.log(data); }, + error => { console.log(error); }); + +The returned reqID can be useful to track whether a request has already been +made (ie. the app is still waiting on a response). + +Please consult the [AVWX documentation](https://avwx.docs.apiary.io/) for +information about the available end-points and request parameters. + + +## Author + +Flaparoo [github](https://github.com/flaparoo) + diff --git a/apps/avwx/avwx.js b/apps/avwx/avwx.js new file mode 100644 index 000000000..1a9193b26 --- /dev/null +++ b/apps/avwx/avwx.js @@ -0,0 +1,47 @@ +/* + * AVWX Bangle Module + * + * AVWX doco: https://avwx.docs.apiary.io/ + * test AVWX API request with eg.: curl -X GET 'https://avwx.rest/api/metar/43.9844,-88.5570?token=...' + * + */ + + +const AVWX_BASE_URL = 'https://avwx.rest/api/'; // must end with a slash +const AVWX_CONFIG_FILE = 'avwx.json'; + + +// read in the settings +var AVWXsettings = Object.assign({ + AVWXtoken: '', +}, require('Storage').readJSON(AVWX_CONFIG_FILE, true) || {}); + + +/** + * Make an AVWX API request + * + * @param {string} requestPath API path (after /api/), eg. 'meta/KOSH' + * @param {string} params optional request parameters, eg. 'onfail=nearest' (use '&' in the string to combine multiple params) + * @param {function} successCB callback if the API request was successful - will supply the returned data: successCB(data) + * @param {function} failCB callback in case the API request failed - will supply the error: failCB(error) + * + * @returns {number} the HTTP request ID + * + * Example: + * reqID = avwx.request('metar/'+lat+','+lon, + * 'filter=sanitized&onfail=nearest', + * data => { console.log(data); }, + * error => { console.log(error); }); + * + */ +exports.request = function(requestPath, optParams, successCB, failCB) { + if (! AVWXsettings.AVWXtoken) { + failCB('No AVWX API Token defined!'); + return undefined; + } + let params = 'token='+AVWXsettings.AVWXtoken; + if (optParams) + params += '&'+optParams; + return Bangle.http(AVWX_BASE_URL+requestPath+'?'+params).then(successCB).catch(failCB); +}; + diff --git a/apps/avwx/avwx.png b/apps/avwx/avwx.png new file mode 100644 index 0000000000000000000000000000000000000000..129c9f9f466038ae3d3688e9768da8392ed4f621 GIT binary patch literal 1809 zcmV+s2k!WZP)UU5qAbvMgSr ze>9o#HqAD5(d+jpMl{oeQezEg!Fj4;9o<9`nT*Xyn3Ck!@mIcGDkXtW8- z^j7N~ifbK>qIfSyiISHD!b<|YNQsM+NF7JXI{>azTx;N|`D0EZj!T65ASVerjvALJ zt`%@phz&+K?=>UBP}7L{@vE&}(P$Odge+bX&}0+QYP0lDyIh;ET1x3hEsye&Fd3}i z;|{Bb#esZas z>%LmGqNrZiU&7ae@z0YTGTHAs<7w!6I61io? zUSi@?5<_mQ{OsDmNy3{87GGx{yqlxK)ZD=v#6dZA{IUF4i=1BiVWVt--5;^~3+zl+ zK@%5^((ivnabzTv(NRcEQzBK9f}G-f6#RJ*WzBlj8+$L*110AE(b-wb5vd-S)21`fK7L7QXDc1@_b&8 ziunblh6bMogl4^bZYUyBK6a*W_i2EoFiYh?!tVkQAx%y0XDcI_9EUjRlvm0V4l1vf zN9~r7i;<;Jfvy4>Y066s^wd^iMCbvDNd#H>720CrP?(niRdW2Lt<^?e4kr2!^*Zf6 zOmO#@B#WNQD>cAUY|9Vp}AE7yB%ds4VN|G@0zZs6Y4Y-H0mUjG}K%^7*_F> zV7ChA14WgIPfCcv@z%p|*iF#=QH0E#?I9W9tY)ZUB604l1v4%?3iC3NSEfa3nhH7E zJ>Ct05?8Cuc9$X;@RHCS$N<~hv~e}^i{0FvrAAVGG%OD!AfJJkYBhWg?&lAsGp`~?GKt3Tm6g}afJro@I-pMVbcdl{gX zevAmcp5v0eIjHPv^D^*zLk*&S-iYo_P8K{qYW5n#P1m%9#H~M}>SVjyz=Ymzel92j zt%TgpQ_?qActuAuvUcr6(Jt2J?3g-dg=wG@$C~Tm`rGDH#Hr&Pl9X{C0|k|(DBG6{ zr-S&se`XF(wCf?7x{(|ciMlhV-8yQ5o`q(~{2(U@t?n0>?9GPVI*92hDhoK;SPi}I zfL{TuHkiTMiV|#Ew?-C_qKXeFd}e+SI(g(*mbvS0rge|Hy&&NC(@L?d$DYIL;KLszMl3wj|2I4Z;`76JpMcXASO2J^BV+5&9e z6oIuXKfv0LK9u>*=O4>kZ&nW>8=yjC-QeJ@iiLuggye;#K~U^w9cQ-ljB=8Y%38Z( zF(9uIl5pzEQs9E90Qcly{0cNKVc#W+#RhLPJ;VO`2r&gDo{BH>lA!jt*W##<^{)u_ zh33)WM}pZi&5CcI`$^bn6`toQ$>1bnn5Bl9Q~cLpJ6B#3zU3$x@FTvL{n^XzQ(Wud z<+KvtH%P)N0~OxmM@)}hZB1c>5k?qcC<5UhX^=o$Y%~I400000NkvXXu0mjf#Cc;# literal 0 HcmV?d00001 diff --git a/apps/avwx/interface.html b/apps/avwx/interface.html new file mode 100644 index 000000000..cdd77cb74 --- /dev/null +++ b/apps/avwx/interface.html @@ -0,0 +1,47 @@ + + + + + + +

To use the AVWX API, you need an account and generate an API token. The free "Hobby" plan is sufficient, but please consider supporting the AVWX project.

+

+ + +

+

+ +

+ +

+ + + + + + diff --git a/apps/avwx/metadata.json b/apps/avwx/metadata.json new file mode 100644 index 000000000..0b07f32d4 --- /dev/null +++ b/apps/avwx/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "avwx", + "name": "AVWX Module", + "shortName":"AVWX", + "version":"1.00", + "description": "Module/library for the AVWX API", + "icon": "avwx.png", + "type": "module", + "tags": "outdoors", + "supports": ["BANGLEJS2"], + "provides_modules": ["avwx"], + "readme": "README.md", + "interface": "interface.html", + "storage": [ + { "name":"avwx", "url":"avwx.js" } + ], + "data": [{ "name":"avwx.json" }] +}