Advertisement
Guest User

Untitled

a guest
Nov 28th, 2020
1,231
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // Variables used by Scriptable.
  2. // These must be at the very top of the file. Do not edit.
  3. // icon-color: deep-purple; icon-glyph: calendar;
  4. /*
  5.  
  6. ~
  7.  
  8. Welcome to Weather Cal. Run this script to set up your widget.
  9.  
  10. Add or remove items from the widget in the layout section below.
  11.  
  12. You can duplicate this script to create multiple widgets. Make sure to change the name of the script each time.
  13.  
  14. Happy scripting!
  15.  
  16. ~
  17.  
  18. */
  19.  
  20. // Specify the layout of the widget items.
  21. const layout = `
  22. row
  23.   column
  24.     date
  25.     space(5)
  26.     aqi
  27.   column(120)
  28.     current
  29.     space(15)
  30.     future
  31.     space(15)
  32.     battery
  33.     sunset
  34.     space(10)
  35. row
  36.   column
  37.     scriptable  
  38. `
  39.  
  40. /*
  41.  * CODE
  42.  * Be more careful editing this section.
  43.  * =====================================
  44.  */
  45.  
  46. // Names of Weather Cal elements.
  47. const codeFilename = "Weather Cal code"
  48. const gitHubUrl = "https://raw.githubusercontent.com/mzeryck/Weather-Cal/main/weather-cal-code.js"
  49.  
  50. // Determine if the user is using iCloud.
  51. let files = FileManager.local()
  52. const iCloudInUse = files.isFileStoredIniCloud(module.filename)
  53.  
  54. // If so, use an iCloud file manager.
  55. files = iCloudInUse ? FileManager.iCloud() : files
  56.  
  57. // Determine if the Weather Cal code exists and download if needed.
  58. const pathToCode = files.joinPath(files.documentsDirectory(), codeFilename + ".js")
  59. if (!files.fileExists(pathToCode)) {
  60.   const req = new Request(gitHubUrl)
  61.   const codeString = await req.loadString()
  62.   files.writeString(pathToCode, codeString)
  63. }
  64.  
  65. // Import the code.
  66. if (iCloudInUse) { await files.downloadFileFromiCloud(pathToCode) }
  67. const code = importModule(codeFilename)
  68.  
  69. const custom = {
  70.  
  71. async scriptable(column) {
  72. // This script shows a random Scriptable API in a widget. The script is meant to be used with a widget configured on the Home Screen.
  73. // You can run the script in the app to preview the widget or you can go to the Home Screen, add a new Scriptable widget and configure the widget to run this script.
  74. // You can also try creating a shortcut that runs this script. Running the shortcut will show widget.
  75. let api = await randomAPI()
  76. let widget = await createWidget(api)
  77. if (config.runsInWidget) {
  78.   // The script runs inside a widget, so we pass our instance of ListWidget to be shown inside the widget on the Home Screen.
  79. } else {
  80.   // The script runs inside the app, so we preview the widget.
  81. }
  82. // This can speed up the execution, in particular when running the script from Shortcuts or using Siri.
  83.  
  84. async function createWidget(api) {
  85.   let appIcon = await loadAppIcon()
  86.   let title = "Random Scriptable API"
  87.   let widget = column.addStack()
  88. widget.layoutVertically()
  89. widget.cornerRadius = 20
  90. widget.setPadding(10, 10, 10, 10)
  91.   // Add background gradient
  92.   let gradient = new LinearGradient()
  93.   gradient.locations = [0, 1]
  94.   gradient.colors = [
  95.     new Color("141414"),
  96.     new Color("13233F")
  97.   ]
  98.   widget.backgroundGradient = gradient
  99.   // Show app icon and title
  100.   let titleStack = widget.addStack()
  101.   let appIconElement = titleStack.addImage(appIcon)
  102.   appIconElement.imageSize = new Size(15, 15)
  103.   appIconElement.cornerRadius = 4
  104.   titleStack.addSpacer(4)
  105.   let titleElement = titleStack.addText(title)
  106.   titleElement.textColor = Color.white()
  107.   titleElement.textOpacity = 0.7
  108.   titleElement.font = Font.mediumSystemFont(13)
  109.   widget.addSpacer(12)
  110.   // Show API
  111.   let nameElement = widget.addText(api.name)
  112.   nameElement.textColor = Color.white()
  113.   nameElement.font = Font.boldSystemFont(18)
  114.   widget.addSpacer(2)
  115.   let descriptionElement = widget.addText(api.description)
  116.   descriptionElement.minimumScaleFactor = 0.5
  117.   descriptionElement.textColor = Color.white()
  118.   descriptionElement.font = Font.systemFont(18)
  119.   // UI presented in Siri ans Shortcuta is non-interactive, so we only show the footer when not running the script from Siri.
  120.   if (!config.runsWithSiri) {
  121.     widget.addSpacer(8)
  122.     // Add button to open documentation
  123.     let linkSymbol = SFSymbol.named("arrow.up.forward")
  124.     let footerStack = widget.addStack()
  125.     let linkStack = footerStack.addStack()
  126.     linkStack.centerAlignContent()
  127.     linkStack.url = api.url
  128.     let linkElement = linkStack.addText("Read more")
  129.     linkElement.font = Font.mediumSystemFont(13)
  130.     linkElement.textColor = Color.blue()
  131.     linkStack.addSpacer(3)
  132.     let linkSymbolElement = linkStack.addImage(linkSymbol.image)
  133.     linkSymbolElement.imageSize = new Size(11, 11)
  134.     linkSymbolElement.tintColor = Color.blue()
  135.     footerStack.addSpacer()
  136.     // Add link to documentation
  137.     let docsSymbol = SFSymbol.named("book")
  138.     let docsElement = footerStack.addImage(docsSymbol.image)
  139.     docsElement.imageSize = new Size(20, 20)
  140.     docsElement.tintColor = Color.white()
  141.     docsElement.imageOpacity = 0.5
  142.     docsElement.url = "https://docs.scriptable.app"
  143.   }
  144. }
  145.  
  146. async function randomAPI() {
  147.   let docs = await loadDocs()
  148.   let apiNames = Object.keys(docs)
  149.   let num = Math.round(Math.random() * apiNames.length)
  150.   let apiName = apiNames[num]
  151.   let api = docs[apiName]
  152.   return {
  153.     name: apiName,
  154.     description: api["!doc"],
  155.     url: api["!url"]
  156.   }
  157. }
  158.  
  159. async function loadDocs() {
  160.   let url = "https://docs.scriptable.app/scriptable.json"
  161.   let req = new Request(url)
  162.   return await req.loadJSON()
  163. }
  164.  
  165. async function loadAppIcon() {
  166.   let url = "https://is5-ssl.mzstatic.com/image/thumb/Purple124/v4/21/1e/13/211e13de-2e74-4221-f7db-d6d2c53b4323/AppIcon-1x_U007emarketing-0-7-0-85-220.png/540x540sr.jpg"
  167.   let req = new Request(url)
  168.   return req.loadImage()
  169. }
  170. },
  171.  
  172. async aqi(column) {
  173. "use strict";
  174.  
  175. /**
  176.  * This widget is from <https://github.com/jasonsnell/PurpleAir-AQI-Scriptable-Widget>
  177.  * By Jason Snell, Rob Silverii, Adam Lickel, Alexander Ogilvie, and Brian Donovan.
  178.  * Based on code by Matt Silverlock.
  179.  */
  180.  
  181. const API_URL = "https://www.purpleair.com";
  182.  
  183. /**
  184.  * Find a nearby PurpleAir sensor ID via https://fire.airnow.gov/
  185.  * Click a sensor near your location: the ID is the trailing integers
  186.  * https://www.purpleair.com/json has all sensors by location & ID.
  187.  * @type {number}
  188.  */
  189. const SENSOR_ID = args.widgetParameter;
  190.  
  191. /**
  192.  * Widget attributes: AQI level threshold, text label, gradient start and end colors, text color
  193.  *
  194.  * @typedef {object} LevelAttribute
  195.  * @property {number} threshold
  196.  * @property {string} label
  197.  * @property {string} startColor
  198.  * @property {string} endColor
  199.  * @property {string} textColor
  200.  * @property {string} darkStartColor
  201.  * @property {string} darkEndColor
  202.  * @property {string} darkTextColor
  203.  */
  204.  
  205. /**
  206.  * @typedef {object} SensorData
  207.  * @property {string} val
  208.  * @property {string} adj1
  209.  * @property {string} adj2
  210.  * @property {number} ts
  211.  * @property {string} hum
  212.  * @property {string} loc
  213.  * @property {string} lat
  214.  * @property {string} lon
  215.  */
  216.  
  217. /**
  218.  * @typedef {object} LatLon
  219.  * @property {number} latitude
  220.  * @property {number} longitude
  221.  */
  222.  
  223. /**
  224.  * Get JSON from a local file
  225.  *
  226.  * @param {string} fileName
  227.  * @returns {object}
  228.  */
  229. function getCachedData(fileName) {
  230.   const fileManager = FileManager.local();
  231.   const cacheDirectory = fileManager.joinPath(fileManager.libraryDirectory(), "jsnell-aqi");
  232.   const cacheFile = fileManager.joinPath(cacheDirectory, fileName);
  233.  
  234.   if (!fileManager.fileExists(cacheFile)) {
  235.     return undefined;
  236.   }
  237.  
  238.   const contents = fileManager.readString(cacheFile);
  239.   return JSON.parse(contents);
  240. }
  241.  
  242. /**
  243.  * Wite JSON to a local file
  244.  *
  245.  * @param {string} fileName
  246.  * @param {object} data
  247.  */
  248. function cacheData(fileName, data) {
  249.   const fileManager = FileManager.local();
  250.   const cacheDirectory = fileManager.joinPath(fileManager.libraryDirectory(), "jsnell-aqi");
  251.   const cacheFile = fileManager.joinPath(cacheDirectory, fileName);
  252.  
  253.   if (!fileManager.fileExists(cacheDirectory)) {
  254.     fileManager.createDirectory(cacheDirectory);
  255.   }
  256.  
  257.   const contents = JSON.stringify(data);
  258.   fileManager.writeString(cacheFile, contents);
  259. }
  260.  
  261. /**
  262.  * Get the closest PurpleAir sensorId to the given location
  263.  *
  264.  * @returns {Promise<number>}
  265.  */
  266. async function getSensorId() {
  267.   if (SENSOR_ID) return SENSOR_ID;
  268.  
  269.   let fallbackSensorId = undefined;
  270.  
  271.   try {
  272.     const cachedSensor = getCachedData("sensor.json");
  273.     if (cachedSensor) {
  274.       console.log({ cachedSensor });
  275.  
  276.       const { id, updatedAt } = cachedSensor;
  277.       fallbackSensorId = id;
  278.       // If we've fetched the location within the last 15 minutes, just return it
  279.       if (Date.now() - updatedAt < 15 * 60 * 1000) {
  280.         return id;
  281.       }
  282.     }
  283.  
  284.     /** @type {LatLon} */
  285.     const { latitude, longitude } = await Location.current();
  286.  
  287.     const BOUND_OFFSET = 0.2;
  288.  
  289.     const nwLat = latitude + BOUND_OFFSET;
  290.     const seLat = latitude - BOUND_OFFSET;
  291.     const nwLng = longitude - BOUND_OFFSET;
  292.     const seLng = longitude + BOUND_OFFSET;
  293.  
  294.     const req = new Request(
  295.       `${API_URL}/json?exclude=true&nwlat=${nwLat}&selat=${seLat}&nwlng=${nwLng}&selng=${seLng}`
  296.     );
  297.  
  298.     /** @type {{ code?: number; results?: Array<Object<string, number|string>>; }} */
  299.     const res = await req.loadJSON();
  300.  
  301.     const { results } = res;
  302.  
  303.     const sensorIdField = "ID";
  304.     const latField = "Lat";
  305.     const lonField = "Lon";
  306.     const locationField = "DEVICE_LOCATIONTYPE";
  307.     const ageField = "AGE";
  308.     const OUTDOOR = "outside";
  309.  
  310.     let closestSensor;
  311.     let closestDistance = Infinity;
  312.  
  313.     for (const location of results.filter((datum) => datum[locationField] === OUTDOOR && datum[ageField] < 60 * 4)) {
  314.       const distanceFromLocation = haversine(
  315.         { latitude, longitude },
  316.         { latitude: location[latField], longitude: location[lonField] }
  317.       );
  318.       if (distanceFromLocation < closestDistance) {
  319.         closestDistance = distanceFromLocation;
  320.         closestSensor = location;
  321.       }
  322.     }
  323.  
  324.     const id = closestSensor[sensorIdField];
  325.     cacheData("sensor.json", { id, updatedAt: Date.now() });
  326.  
  327.     return id;
  328.   } catch (error) {
  329.     console.log(`Could not fetch location: ${error}`);
  330.     return fallbackSensorId;
  331.   }
  332. }
  333.  
  334. /**
  335.  * Returns the haversine distance between start and end.
  336.  *
  337.  * @param {LatLon} start
  338.  * @param {LatLon} end
  339.  * @returns {number}
  340.  */
  341. function haversine(start, end) {
  342.   const toRadians = (n) => (n * Math.PI) / 180;
  343.  
  344.   const deltaLat = toRadians(end.latitude - start.latitude);
  345.   const deltaLon = toRadians(end.longitude - start.longitude);
  346.   const startLat = toRadians(start.latitude);
  347.   const endLat = toRadians(end.latitude);
  348.  
  349.   const angle =
  350.     Math.sin(deltaLat / 2) ** 2 +
  351.     Math.sin(deltaLon / 2) ** 2 * Math.cos(startLat) * Math.cos(endLat);
  352.  
  353.   return 2 * Math.atan2(Math.sqrt(angle), Math.sqrt(1 - angle));
  354. }
  355.  
  356. /**
  357.  * Fetch content from PurpleAir
  358.  *
  359.  * @param {number} sensorId
  360.  * @returns {Promise<SensorData>}
  361.  */
  362. async function getSensorData(sensorId) {
  363.   const sensorCache = `sensor-${sensorId}-data.json`;
  364.   const req = new Request(`${API_URL}/json?show=${sensorId}`);
  365.   let json = await req.loadJSON();
  366.  
  367.   try {
  368.     // Check that our results are what we expect
  369.     if (json && json.results && Array.isArray(json.results) && json.results.length > 1) {
  370.       console.log(`Sensor data looks good, will cache.`);
  371.       const sensorData = { json, updatedAt: Date.now() }
  372.       cacheData(sensorCache, sensorData);
  373.     } else {
  374.       const { json: cachedJson, updatedAt } = getCachedData(sensorCache);
  375.       if (Date.now() - updatedAt > 2 * 60 * 60 * 1000) {
  376.         // Bail if our data is > 2 hours old
  377.         throw `Our cache is too old: ${updatedAt }`;
  378.       }
  379.       console.log(`Using cached sensor data: ${updatedAt}`);
  380.       json = cachedJson;
  381.     }
  382.     return {
  383.       val: json.results[0].Stats,
  384.       adj1: json.results[0].pm2_5_cf_1,
  385.       adj2: json.results[1].pm2_5_cf_1,
  386.       ts: json.results[0].LastSeen,
  387.       hum: json.results[0].humidity,
  388.       loc: json.results[0].Label,
  389.       lat: json.results[0].Lat,
  390.       lon: json.results[0].Lon,
  391.     };
  392.   } catch (error) {
  393.     console.log(`Could not parse JSON: ${error}`);
  394.     throw 666;
  395.   }
  396. }
  397.  
  398. /**
  399.  * Fetch reverse geocode
  400.  *
  401.  * @param {string} lat
  402.  * @param {string} lon
  403.  * @returns {Promise<GeospatialData>}
  404.  */
  405. async function getGeoData(lat, lon) {
  406.   const latitude = Number.parseFloat(lat);
  407.   const longitude = Number.parseFloat(lon);
  408.  
  409.   const geo = await Location.reverseGeocode(latitude, longitude);
  410.   console.log({ geo: geo });
  411.  
  412.   return {
  413.     neighborhood: geo[0].subLocality,
  414.     city: geo[0].locality,
  415.     state: geo[0].administrativeArea,
  416.   };
  417. }
  418.  
  419. /**
  420.  * Fetch a renderable location
  421.  *
  422.  * @param {SensorData} data
  423.  * @returns {Promise<String>}
  424.  */
  425. async function getLocation(data) {
  426.   try {
  427.     if (args.widgetParameter) {
  428.       return data.loc;
  429.     }
  430.  
  431.     const geoData = await getGeoData(data.lat, data.lon);
  432.     console.log({ geoData });
  433.  
  434.     if (geoData.neighborhood && geoData.city) {
  435.         return `${geoData.neighborhood}, ${geoData.city}`;
  436.     } else {
  437.         return geoData.city || data.loc;
  438.     }
  439.   } catch (error) {
  440.     console.log(`Could not cleanup location: ${error}`);
  441.     return data.loc;
  442.   }
  443. }
  444.  
  445. /** @type {Array<LevelAttribute>} sorted by threshold desc. */
  446. const LEVEL_ATTRIBUTES = [
  447.   {
  448.     threshold: 300,
  449.     label: "Hazardous",
  450.     startColor: "76205d",
  451.     endColor: "521541",
  452.     textColor: "f0f0f0",
  453.     darkStartColor: "333333",
  454.     darkEndColor: "000000",
  455.     darkTextColor: "ce4ec5",
  456.   },
  457.   {
  458.     threshold: 200,
  459.     label: "Very Unhealthy",
  460.     startColor: "9c2424",
  461.     endColor: "661414",
  462.     textColor: "f0f0f0",
  463.     darkStartColor: "333333",
  464.     darkEndColor: "000000",
  465.     darkTextColor: "f33939",
  466.   },
  467.   {
  468.     threshold: 150,
  469.     label: "Unhealthy",
  470.     startColor: "da5340",
  471.     endColor: "bc2f26",
  472.     textColor: "eaeaea",
  473.     darkStartColor: "333333",
  474.     darkEndColor: "000000",
  475.     darkTextColor: "f16745",
  476.   },
  477.   {
  478.     threshold: 100,
  479.     label: "Unhealthy for Sensitive Groups",
  480.     startColor: "f5ba2a",
  481.     endColor: "d3781c",
  482.     textColor: "1f1f1f",
  483.     darkStartColor: "333333",
  484.     darkEndColor: "000000",
  485.     darkTextColor: "f7a021",
  486.   },
  487.   {
  488.     threshold: 50,
  489.     label: "Moderate",
  490.     startColor: "f2e269",
  491.     endColor: "dfb743",
  492.     textColor: "1f1f1f",
  493.     darkStartColor: "333333",
  494.     darkEndColor: "000000",
  495.     darkTextColor: "f2e269",
  496.   },
  497.   {
  498.     threshold: -20,
  499.     label: "Good",
  500.     startColor: "8fec74",
  501.     endColor: "77c853",
  502.     textColor: "1f1f1f",
  503.     darkStartColor: "333333",
  504.     darkEndColor: "000000",
  505.     darkTextColor: "6de46d",
  506.   },
  507. ];
  508.  
  509.  
  510. /**
  511.  * Get the EPA adjusted PPM
  512.  *
  513.  * @param {SensorData} sensorData
  514.  * @returns {number} EPA adjustment for wood smoke and PurpleAir from slide 8 of https://cfpub.epa.gov/si/si_public_record_report.cfm?dirEntryId=349513&Lab=CEMM&simplesearch=0&showcriteria=2&sortby=pubDate&timstype=&datebeginpublishedpresented=08/25/2018
  515.  */
  516. function computePM(sensorData) {
  517.   const adj1 = Number.parseInt(sensorData.adj1, 10);
  518.   const adj2 = Number.parseInt(sensorData.adj2, 10);
  519.   const hum = Number.parseInt(sensorData.hum, 10);
  520.   const dataAverage = (adj1 + adj2) / 2;
  521.   console.log(`PM2.5 number is ${dataAverage}.`)
  522.   if (dataAverage < 250) {
  523.     console.log(`Using EPA calculation.`)
  524.     return 0.52 * dataAverage - 0.085 * hum + 5.71;
  525.   } else {
  526.     console.log(`Using AQANDU calculation.`)
  527.     return .0778 * dataAverage + 2.65
  528.   }
  529. }
  530.  
  531. /**
  532.  * Get AQI number from PPM reading
  533.  *
  534.  * @param {number} pm
  535.  * @returns {number|'-'}
  536.  */
  537. function aqiFromPM(pm) {
  538.   if (pm > 350.5) return calculateAQI(pm, 500.0, 401.0, 500.0, 350.5);
  539.   if (pm > 250.5) return calculateAQI(pm, 400.0, 301.0, 350.4, 250.5);
  540.   if (pm > 150.5) return calculateAQI(pm, 300.0, 201.0, 250.4, 150.5);
  541.   if (pm > 55.5) return calculateAQI(pm, 200.0, 151.0, 150.4, 55.5);
  542.   if (pm > 35.5) return calculateAQI(pm, 150.0, 101.0, 55.4, 35.5);
  543.   if (pm > 12.1) return calculateAQI(pm, 100.0, 51.0, 35.4, 12.1);
  544.   if (pm >= 0.0) return calculateAQI(pm, 50.0, 0.0, 12.0, 0.0);
  545.   return "-";
  546. }
  547.  
  548. /**
  549.  * Calculate the AQI number
  550.  *
  551.  * @param {number} Cp
  552.  * @param {number} Ih
  553.  * @param {number} Il
  554.  * @param {number} BPh
  555.  * @param {number} BPl
  556.  * @returns {number}
  557.  */
  558. function calculateAQI(Cp, Ih, Il, BPh, BPl) {
  559.   const a = Ih - Il;
  560.   const b = BPh - BPl;
  561.   const c = Cp - BPl;
  562.   return Math.round((a / b) * c + Il);
  563. }
  564.  
  565. /**
  566.  * Calculates the AQI level
  567.  * based on https://cfpub.epa.gov/airnow/index.cfm?action=aqibasics.aqi#unh
  568.  *
  569.  * @param {number|'-'} aqi
  570.  * @returns {LevelAttribute & { level: number }}
  571.  */
  572. function calculateLevel(aqi) {
  573.   const level = Number(aqi) || 0;
  574.  
  575.   const {
  576.     label = "Weird",
  577.     startColor = "white",
  578.     endColor = "white",
  579.     textColor = "black",
  580.     darkStartColor = "009900",
  581.     darkEndColor = "007700",
  582.     darkTextColor = "000000",
  583.     threshold = -Infinity,
  584.   } = LEVEL_ATTRIBUTES.find(({ threshold }) => level > threshold) || {};
  585.  
  586.   return {
  587.     label,
  588.     startColor,
  589.     endColor,
  590.     textColor,
  591.     darkStartColor,
  592.     darkEndColor,
  593.     darkTextColor,
  594.     threshold,
  595.     level,
  596.   };
  597. }
  598.  
  599. /**
  600.  * Get the AQI trend
  601.  *
  602.  * @param {{ v1: number; v3: number; }} stats
  603.  * @returns {string}
  604.  */
  605. function getAQITrend({ v1: partLive, v3: partTime }) {
  606.   const partDelta = partTime - partLive;
  607.   if (partDelta > 5) return "arrow.down";
  608.   if (partDelta < -5) return "arrow.up";
  609.   return "arrow.left.and.right";
  610. }
  611.  
  612. /**
  613.  * Constructs an SFSymbol from the given symbolName
  614.  *
  615.  * @param {string} symbolName
  616.  * @returns {object} SFSymbol
  617.  */
  618. function createSymbol(symbolName) {
  619.   const symbol = SFSymbol.named(symbolName);
  620.   symbol.applyFont(Font.systemFont(15));
  621.   return symbol;
  622. }
  623.  
  624. async function run() {
  625.   const listWidget = column.addStack()
  626. listWidget.layoutVertically()
  627. listWidget.cornerRadius = 20
  628. listWidget.setPadding(10, 10, 10, 10);
  629.   listWidget.setPadding(10, 15, 10, 10);
  630.  
  631.   try {
  632.      const sensorId = await getSensorId();
  633.  
  634.     if (!sensorId) {
  635.       throw "Please specify a location for this widget.";
  636.     }
  637.     console.log(`Using sensor ID: ${sensorId}`);
  638.  
  639.     const data = await getSensorData(sensorId);
  640.  
  641.     const stats = JSON.parse(data.val);
  642.     console.log({ stats });
  643.  
  644.     const aqiTrend = getAQITrend(stats);
  645.     console.log({ aqiTrend });
  646.  
  647.     const epaPM = computePM(data);
  648.     console.log({ epaPM });
  649.  
  650.     const aqi = aqiFromPM(epaPM);
  651.     const level = calculateLevel(aqi);
  652.     const aqiText = aqi.toString();
  653.     console.log({ aqi });
  654.  
  655.     const sensorLocation = await getLocation(data)
  656.     console.log({ sensorLocation });
  657.  
  658.     const startColor = Color.dynamic(new Color(level.startColor), new Color(level.darkStartColor));
  659.          
  660.     const endColor = Color.dynamic(new Color(level.endColor), new Color(level.darkEndColor));
  661.    
  662.     const textColor = Color.dynamic(new Color(level.textColor), new Color(level.darkTextColor));
  663.    
  664.     const gradient = new LinearGradient();
  665.  
  666.     gradient.colors = [startColor, endColor];
  667.     gradient.locations = [0.0, 1];
  668.     console.log({ gradient });
  669.  
  670.     listWidget.backgroundGradient = gradient;
  671.  
  672.     const header = listWidget.addText('Air Quality'.toUpperCase());
  673.     header.textColor = textColor;
  674.     header.font = Font.regularSystemFont(11);
  675.     header.minimumScaleFactor = 0.50;
  676.  
  677.     const wordLevel = listWidget.addText(level.label);
  678.     wordLevel.textColor = textColor;
  679.     wordLevel.font = Font.semiboldSystemFont(25);
  680.     wordLevel.minimumScaleFactor = 0.3;
  681.  
  682.     listWidget.addSpacer(5);
  683.  
  684.     const scoreStack = listWidget.addStack();
  685.     const content = scoreStack.addText(aqiText);
  686.     content.textColor = textColor;
  687.     content.font = Font.semiboldSystemFont(30);
  688.     const trendSymbol = createSymbol(aqiTrend);
  689.     const trendImg = scoreStack.addImage(trendSymbol.image);
  690.     trendImg.resizable = false;
  691.     trendImg.tintColor = textColor;
  692.     trendImg.imageSize = new Size(28, 30);
  693.  
  694.     listWidget.addSpacer(10);
  695.  
  696.     const locationText = listWidget.addText(sensorLocation);
  697.     locationText.textColor = textColor;
  698.     locationText.font = Font.regularSystemFont(14);
  699.     locationText.minimumScaleFactor = 0.5;
  700.  
  701.     listWidget.addSpacer(2);
  702.  
  703.     const updatedAt = new Date(data.ts * 1000).toLocaleTimeString([], {
  704.       hour: "numeric",
  705.       minute: "2-digit",
  706.     });
  707.     const widgetText = listWidget.addText(`Updated ${updatedAt}`);
  708.     widgetText.textColor = textColor;
  709.     widgetText.font = Font.regularSystemFont(9);
  710.     widgetText.minimumScaleFactor = 0.6;
  711.  
  712.     const purpleMapUrl = `https://www.purpleair.com/map?opt=1/i/mAQI/a10/cC5&select=${sensorId}#14/${data.lat}/${data.lon}`;
  713.     listWidget.url = purpleMapUrl;
  714.   } catch (error) {
  715.     if (error === 666) {
  716.       // Handle JSON parsing errors with a custom error layout
  717.  
  718.       listWidget.background = new Color('999999');
  719.       const header = listWidget.addText('Error'.toUpperCase());
  720.       header.textColor = new Color('000000');
  721.       header.font = Font.regularSystemFont(11);
  722.       header.minimumScaleFactor = 0.50;
  723.  
  724.       listWidget.addSpacer(15);
  725.  
  726.       const wordLevel = listWidget.addText(`Couldn't connect to the server.`);
  727.      wordLevel.textColor = new Color ('000000');
  728.      wordLevel.font = Font.semiboldSystemFont(15);
  729.      wordLevel.minimumScaleFactor = 0.3;
  730.    } else {
  731.      console.log(`Could not render widget: ${error}`);
  732.  
  733.      const errorWidgetText = listWidget.addText(`${error}`);
  734.      errorWidgetText.textColor = Color.red();
  735.      errorWidgetText.textOpacity = 30;
  736.      errorWidgetText.font = Font.regularSystemFont(10);
  737.    }
  738.  }
  739.  
  740.  if (config.runsInApp) {
  741.  }
  742.  
  743. }
  744.  
  745. await run();
  746. },
  747.  
  748. }
  749.  
  750. // Run the initial setup or settings menu.
  751. let preview
  752. if (config.runsInApp) {
  753.  preview = await code.runSetup(Script.name(), iCloudInUse, codeFilename, gitHubUrl)
  754.  if (!preview) return
  755. }
  756.  
  757. // Set up the widget.
  758. const widget = await code.createWidget(layout, Script.name(), iCloudInUse, custom)
  759. widget.setPadding(0,0,0,0)
  760. Script.setWidget(widget)
  761.  
  762. // If we're in app, display the preview.
  763. if (config.runsInApp) {
  764.   if (preview == "small") { widget.presentSmall() }
  765.   else if (preview == "medium") { widget.presentMedium() }
  766.   else { widget.presentLarge() }
  767. }
  768.  
  769. Script.complete()
  770.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement