Last active
January 20, 2024 06:24
-
-
Save coughski/43c7a4da3829a3ffe394d6eeb6a8c90a to your computer and use it in GitHub Desktop.
Scriptable widget for tracking the status of a Citi Bike station
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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