Skip to content

Instantly share code, notes, and snippets.

@coughski
Last active January 20, 2024 06:24
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save coughski/43c7a4da3829a3ffe394d6eeb6a8c90a to your computer and use it in GitHub Desktop.
Save coughski/43c7a4da3829a3ffe394d6eeb6a8c90a to your computer and use it in GitHub Desktop.
Scriptable widget for tracking the status of a Citi Bike station
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: blue; icon-glyph: bicycle;
/*** WIDGET SETUP ***
* Edit the Scriptable widget, select this CitiBike script, and fill the Parameter field with your station's id
*
* HOW TO FIND A STATION'S ID
* Search for a CitiBike station by street name at https://gbfs.citibikenyc.com/gbfs/en/station_information.json
* Find the station_id field associated with the name
* You can also look up station names on a map in the official CitiBike app
* Paste the station_id (without double quotes "") into the widget's Parameter field. For example: 457
*
* TIPS
* Create a separate widget for every CitiBike station you want to track
* Set the widget's "When Interacting" parameter to "Run Script" to force the widget to reload in Scriptable
* To use with Siri, create a shortcut with a Text action containing the station's id, and set it as the parameter for a Scriptable action running this script
*/
var station_id = "457"
if (config.runsInWidget && args.widgetParameter) {
let widgetParam = args.widgetParameter.toString().trim().replaceAll("\"", "")
if (widgetParam.length > 0) {
station_id = widgetParam
}
} else if (config.runsWithSiri && args.shortcutParameter) {
let param = args.shortcutParameter.toString().trim().replaceAll("\"", "")
if (param.length > 0) {
station_id = param
}
}
const STATION_STATUS_DATA = "https://gbfs.citibikenyc.com/gbfs/en/station_status.json"
const STATION_INFO_DATA = "https://gbfs.citibikenyc.com/gbfs/en/station_information.json"
const DEBUG_WIDGET = false
let station_status = await loadData(STATION_STATUS_DATA)
let station_info = await loadData(STATION_INFO_DATA)
if (config.runsInWidget) {
let widget = await createWidget(station_status, station_info)
Script.setWidget(widget)
} else if (config.runsWithSiri) {
let table = createTable(station_status, station_info)
await QuickLook.present(table)
let speech = createSpeech(station_status, station_info)
Speech.speak(speech)
} else if (DEBUG_WIDGET) {
// Running in the app, present a preview of the widget
let widget = await createWidget(station_status, station_info)
await widget.presentSmall()
} else {
let table = createTable(station_status, station_info)
await QuickLook.present(table)
console.log(createSpeech(station_status, station_info))
}
Script.complete()
function createSpeech(status, info) {
let bikes = status["num_bikes_available"]
let ebikes = status["num_ebikes_available"]
let station = info["name"]
return "There are " + bikes + " bikes and " + ebikes + " E bikes available at " + station + " as of " + formatTime(convertDate(status["last_reported"]))
}
function createTable(status, info) {
let table = new UITable()
table.showSeparators = true
let header = new UITableRow()
header.addText(info["name"], formatTime(convertDate(status["last_reported"])))
header.isHeader = true
table.addRow(header)
let bikeRow = new UITableRow()
let img1 = bikeRow.addImage(SFSymbol.named("bicycle").image)
img1.widthWeight = 2
let bikeCell = bikeRow.addText(status["num_bikes_available"].toString())
bikeCell.leftAligned()
bikeCell.widthWeight = 1
let space1 = bikeRow.addText()
space1.widthWeight = 10
table.addRow(bikeRow)
let ebikeRow = new UITableRow()
let img2 = ebikeRow.addImage(SFSymbol.named("bolt").image)
img2.widthWeight = 2
let ebikeCell = ebikeRow.addText(status["num_ebikes_available"].toString())
ebikeCell.leftAligned()
ebikeCell.widthWeight = 1
let space2 = ebikeRow.addText()
space2.widthWeight = 10
table.addRow(ebikeRow)
return table
}
async function loadData(url) {
let req = new Request(url)
let json = await req.loadJSON()
let stations = json["data"]["stations"]
for (let station of stations) {
if (station["station_id"] == station_id) {
return station
}
}
}
function convertDate(seconds) {
return new Date(seconds * 1000)
}
function formatTime(date) {
var hours = date.getHours()
var minutes = date.getMinutes()
var ampm = hours >= 12 ? "PM" : "AM"
// Convert from military time
hours %= 12
// Display 0 as 12
hours = hours == 0 ? 12 : hours
// Adjust display of minutes
minutes = minutes < 10 ? "0" + minutes : minutes
return hours + ":" + minutes + " " + ampm
}
async function createWidget(status, info) {
let darkBlue = new Color("#333d72", 1)
let darkBlueLighter = new Color("#333d72", 0.6)
let lightBlue = new Color("#3d99ce", 1)
let gradient = new LinearGradient()
gradient.colors = [Color.dynamic(Color.white(), darkBlueLighter), Color.dynamic(Color.white(), darkBlue)]
gradient.locations = [0, 1]
let w = new ListWidget()
w.backgroundGradient = gradient
let symbolColor = Color.dynamic(lightBlue, Color.white())
let numberColor = Color.dynamic(darkBlue, Color.white())
let captionColor = Color.dynamic(darkBlue, Color.white())
let minScaleFactor = 0.9
let headerOpacity = 0.7
let missingOpacity = 0.4
let header = w.addStack()
header.centerAlignContent()
header.layoutVertically()
header.setPadding(20, 0, 0, 0)
let street = header.addText(info["name"])
street.font = Font.caption1()
street.textColor = captionColor
street.textOpacity = headerOpacity
street.minimumScaleFactor = minScaleFactor
street.centerAlignText()
header.addSpacer(2)
date = header.addDate(convertDate(status["last_reported"]))
date.applyTimeStyle()
date.leftAlignText()
date.font = Font.caption1()
date.textColor = captionColor
date.textOpacity = headerOpacity
date.minimumScaleFactor = minScaleFactor
w.addSpacer()
// bike section
let bike_stack = w.addStack()
bike_stack.centerAlignContent()
bike_stack.setPadding(0, 0, 0, 0)
bike_stack.addSpacer()
let bikes = status["num_bikes_available"]
let bike_opacity = bikes > 0 ? 1 : missingOpacity
let symbolFont = Font.systemFont(60)
let symbolSize = new Size(50, 50)
let labelFont = Font.lightSystemFont(32)
let bike = SFSymbol.named("bicycle")
bike.applyFont(symbolFont)
let img = bike_stack.addImage(bike.image)
img.imageSize = new Size(50, 50)
img.tintColor = symbolColor
img.imageOpacity = bike_opacity
bike_stack.addSpacer()
let bike_text = bikes > 0 ? bikes.toString() : "-"
let txt = bike_stack.addText(bike_text)
txt.font = labelFont
txt.textColor = numberColor
txt.textOpacity = bike_opacity
bike_stack.addSpacer()
// ebike section
let ebike_stack = w.addStack()
ebike_stack.centerAlignContent()
ebike_stack.setPadding(0, 0, 10, 0)
ebike_stack.addSpacer()
let ebikes = status["num_ebikes_available"]
let ebike_opacity = ebikes > 0 ? 1 : missingOpacity
let ebike = SFSymbol.named("bolt")
ebike.applyFont(symbolFont)
ebike.applyLightWeight()
let img2 = ebike_stack.addImage(ebike.image)
img2.imageSize = new Size(50, 45)
img2.tintColor = symbolColor
img2.imageOpacity = ebike_opacity
ebike_stack.addSpacer()
let ebike_text = ebikes > 0 ? ebikes.toString() : "-"
let txt2 = ebike_stack.addText(ebike_text)
txt2.font = labelFont
txt2.textColor = numberColor
txt2.textOpacity = ebike_opacity
ebike_stack.addSpacer()
w.addSpacer()
return w
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment