Weather Cal Custom Component

I’ve been using the iOS Scriptable app and the fantastic Weather Cal widget to have an information rich homescreen for a while now. The built-in COVID widget only had data to the country level but I wanted to see the New York Times’ data at the US county level.

OK, so “New York City” isn’t technically a county but the Times makes an exception and groups the boroughs together—call it a hometown bias.

This also serves as a decent example of how to make a custom Weather Cal component that loads, caches, and displays data from a custom API.

The New York Times doesn’t present a REST api but they do publish their data to Github about 3 times a day!

#!/bin/bash
# Live moving averages
curl https://raw.githubusercontent.com/nytimes/covid-19-data/master/rolling-averages/us-counties-recent.csv -o data.csv

# Example of the CSV data:
# date,geoid,county,state,cases,cases_avg,cases_avg_per_100k,deaths,deaths_avg,deaths_avg_per_100k
# 2021-12-21,USA-36998,New York City,New York,17958,10049.14,120.54,33,15.43,0.19

# Data.csv holds the last month of data for every US county sorted in ascending date order.
# I only care about the most recent date so we can search it backwards using tac and grep
tac data.csv | grep -m 1 "New York City" > current_cases_avg.txt

# Grab the 6th column
awk -F "\"*,\"*" '{print $6}' current_cases_avg.txt > current_cases_avg_only.txt

# Copy the text file to my webserver
cp current_cases_avg_only.txt /var/www/FAKE/PATH/current_cases_avg_only.txt

This bash script can live in cron and run every hour.

15 * * * * ~/update.sh >/dev/null 2>&1

In Scriptable, weather-cal.js in the existing custom = {} variable add the following:

  async setupDanielC19() {
    const danielC19Path = code.fm.joinPath(code.fm.libraryDirectory(), "weather-cal-danielC19")
    // getCache(path REQUIRED, minTime OPTIONAL, maxTime OPTIONAL)
    let danielC19Data = code.getCache(danielC19Path, 20, 60)

    if (!danielC19Data || danielC19Data.length == 0 || danielC19Data.cacheExpired) 
      try {

		 //       
      	 // NOTE: Be sure to put in a real URL here!
      	 //
        let rawRequest = new Request("https://danielbeadle.net/FAKE/PATH/current_cases_avg_only.txt")
        let rawData = await rawRequest.loadString()
        if (!rawData || rawData.length == 0) { throw 0 }

        danielC19Data = Number(rawData)

        // Write Data to cache
        code.fm.writeString(danielC19Path, JSON.stringify(danielC19Data, null, 2))

      } catch (err) {
        danielC19Data = code.getCache(danielC19Path, 20, 60)
      }
  
    return danielC19Data
  },
  
  async danielC19(column) {
    if (!this.danielC19Data) {
      danielC19Data = await custom.setupDanielC19()
    }

    // Set up the stack
    let danielC19Stack = code.align(column)
    danielC19Stack.layoutHorizontally()
    danielC19Stack.centerAlignContent()
    danielC19Stack.setPadding(code.padding/2, code.padding*2, code.padding/2, code.padding)
  
    // Round to an integer and add commas
    const c19rkiLine = code.provideText(`NYC 7-Day Avg: ${Number.parseInt(danielC19Data).toLocaleString('en-US')}`, danielC19Stack)
  },

And finally at the top of weather-cal.js add the new component

const layout = `
  row 
    column
      date
      sunset
      battery
      danielC19
      space
      events
`