Created
July 4, 2024 19:45
-
-
Save jasonsnell/239f8d8c3cfc113d7461d57a8887ea79 to your computer and use it in GitHub Desktop.
Weather graph widget for Scriptable
This file contains 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
// Based on code by Efrén Gámez <https://gist.github.com/ImGamez/a8f9d77bf660d7703cc96fee87cdc4b0> | |
// and modified by Max Zeryck <https://talk.automators.fm/t/widget-examples/7994/217> | |
// this version by Jason Snell. parsing weatherkit data directly is beyond the scope of this widget; | |
// it loads a dumped weatherkit JSON from a remote server. Supply your own. | |
// This widget also loads live data from a weather station, again in a custom format. | |
// You will need to replace both of these data sources in order to get weather other than mine. | |
const highTemps = [ ] | |
const dailyConditions = [ ] | |
const dailyPrecip = [ ] | |
const forecastDates = [ ] | |
const precipChance = [ ] | |
const PREVIEW_REGULAR_WIDGET = true | |
const PREVIEW_CIRCULAR_WIDGET = false | |
const PREVIEW_RECTANGULAR_WIDGET = false | |
async function getSensorData() { | |
let req = new Request(`https://snell.zone/weather/weatherjson.php`) | |
let json = await req.loadJSON() | |
let stats = json.data[0] | |
currentTemp = stats.outsidetemp | |
return { | |
"lo": stats.lo, | |
"hum": stats.hum, | |
"rainrate": stats.rainrate, | |
"hi": stats.hi, | |
"insidetemp": stats.insidetemp, | |
"dailyrain": stats.dailyrain, | |
"insidehum": stats.insidehumidity, | |
"temp": stats.outsidetemp, | |
"hourdelta": stats.hourdelta, | |
"dailydelta": stats.dailydelta, | |
"lastupdate": stats.lastupdate, | |
"updateutc": stats.utc | |
}; | |
} | |
// Design presets | |
// units : string > Defines the unit used to measure the temps, for temperatures in Fahrenheit use "imperial", "metric" for Celcius and "standard" for Kelvin (Default: "metric"). | |
const units = "imperial" | |
// roundedGraph : true|false > true (Use rounded values to draw the graph) | false (Draws the graph using decimal values, this can be used to draw an smoother line). | |
const roundedGraph = true | |
// roundedTemp : true|false > true (Displays the temps rounding the values (29.8 = 30 | 29.3 = 29). | |
const roundedTemp = true | |
// daysToShow : (Default: 3 for the small widget and 8 for the medium one). | |
const daysToShow = (config.widgetFamily == "small") ? 3 : 8; | |
// spaceBetweenDays : number > Size of the space between the temps in the graph in pixels. (Default: 60 for the small widget and 44 for the medium one). | |
const spaceBetweenDays = (config.widgetFamily == "small") ? 60 : 75; | |
// Widget Size !important. | |
// Since the widget works "making" an image and displaying it as the widget background, you need to specify the exact size of the widget to | |
// get an 1:1 display ratio, if you specify an smaller size than the widget itself it will be displayed blurry. | |
// You can get the size simply taking an screenshot of your widgets on the home screen and measuring them in an image-proccessing software. | |
// contextSize : number > Height of the widget in screen pixels, this depends on you screen size (for an 4 inch display the small widget is 282 * 282 pixels on the home screen) | |
const contextSize = 320 | |
// mediumWidgetWidth : number > Width of the medium widget in pixels, this depends on you screen size (for an 4 inch display the medium widget is 584 pixels long on the home screen) | |
const mediumWidgetWidth = 712 | |
// accentColor : Color > Accent color of some elements (Graph lines and the location label). | |
const accentColor = new Color("#DDE7D5", 1) | |
// backgroundColor : Color > Background color of the widgets. | |
const backgroundColor = new Color("#6A62CD", 1) | |
// Position and size of the elements on the widget. | |
// All coordinates make reference to the top-left of the element. | |
// highLowCoords : Point > Define the position in pixels of the highs and lows label. | |
const highLowCoords = new Point(30, 20) | |
// highLowFontSize : number > Size in pixels of the font of the highs and lows label. | |
const highLowFontSize = 26 | |
// weatherDescriptionCoords : Point > Position of the weather description label in pixels. | |
const weatherDescriptionCoords = new Point(30, 50) | |
// weatherDescriptionFontSize : number > Font size of the weather description label. | |
const weatherDescriptionFontSize = 22 | |
//footerFontSize : number > Font size of the footer labels (temperature change). | |
const footerFontSize = 22 | |
//todayTempChangeCoords : Point > Coordinates of the "warmer today" label. | |
const todayTempChangeCoords = new Point(30, 275) | |
//hourlyDeltaTimePosAndSize : Rect > Defines the coordinates and size of the hourly delta label. | |
const hourlyDeltaTimePosAndSize = new Rect((config.widgetFamily == "small") ? 150 : 575, 275, 100, footerFontSize+1) | |
//From here proceed with caution. | |
let weatherData; | |
try { | |
req = await new Request(`https://snell.zone/weather/weatherkit.json`) | |
let json = await req.loadJSON() | |
let stats = json.forecastDaily | |
var restOfDayPrecip = stats.days[0].restOfDayForecast.precipitationType; | |
var restOfDayChance = stats.days[0].restOfDayForecast.precipitationChance; | |
var restOfDay = stats.days[0].restOfDayForecast.conditionCode; | |
//console.log(restOfDayChance) | |
for(let i = 0; i<10 ;i++){ | |
highTemps.push (cToF(stats.days[i].temperatureMax)); | |
dailyConditions.push (stats.days[i].conditionCode); | |
dailyPrecip.push (stats.days[i].precipitationType); | |
forecastDates.push (stats.days[i].forecastStart); | |
precipChance.push (stats.days[i].precipitationChance); | |
}; | |
}catch(e){ | |
console.log(e) | |
} | |
let data = await getSensorData() | |
high = Math.round(data.hi) + '°' | |
low = Math.round(data.lo) + '°' | |
if (config.runsInWidget) { | |
let widget = null | |
if (config.widgetFamily == "accessoryCircular") { | |
widget = createCircularWidget(data.temp) | |
} else if (config.widgetFamily == "accessoryRectangular") { | |
// nothing | |
} else if (config.widgetFamily == "accessoryInline") { | |
// nothing | |
} else if (config.widgetFamily == "medium") { | |
widget = createMediumWidget() | |
} else { | |
widget = createMediumWidget() | |
} | |
Script.setWidget(widget) | |
Script.complete() | |
} else if (PREVIEW_REGULAR_WIDGET) { | |
let widget = createMediumWidget() | |
await widget.presentMedium() | |
} else if (PREVIEW_CIRCULAR_WIDGET) { | |
let widget = createCircularWidget(data.temp, wordForCondition(restOfDay)) | |
await widget.presentSmall() | |
} | |
function createMediumWidget() { | |
let drawContext = new DrawContext(); | |
drawContext.size = new Size((config.widgetFamily == "small") ? contextSize : mediumWidgetWidth, contextSize) | |
drawContext.opaque = false | |
drawContext.setTextAlignedCenter() | |
let widget = new ListWidget(); | |
widget.setPadding(0, 0, 0, 0); | |
widget.backgroundColor = backgroundColor; | |
widget.url = 'https://snell.zone/weather/weather.php' | |
if (config.widgetFamily == "small") { | |
drawText((Math.round(data.temp) + '°'), 60, 210, 20, Color.white()); | |
drawText((`Hi ${high}/Lo ${low}`), highLowFontSize, highLowCoords.x, highLowCoords.y, Color.white()); | |
} else { | |
drawText((Math.round(data.temp) + '°'), 50, 600, 20, Color.white()); | |
drawText((`High ${high} / Low ${low}`), highLowFontSize, highLowCoords.x, highLowCoords.y, Color.white()); | |
} | |
if (data.dailyrain > 0) { | |
drawText(wordForCondition(restOfDay) + ' (' + data.dailyrain + '" rain today)', weatherDescriptionFontSize, weatherDescriptionCoords.x, weatherDescriptionCoords.y, Color.white()) | |
} else { | |
drawText(wordForCondition(restOfDay), weatherDescriptionFontSize, weatherDescriptionCoords.x, weatherDescriptionCoords.y, Color.white()) | |
} | |
let min, max, diff; | |
for(let i = 0; i<=daysToShow ;i++){ | |
let temp = shouldRound(roundedGraph, highTemps[i]); | |
min = (temp < min || min == undefined ? temp : min) | |
max = (temp > max || max == undefined ? temp : max) | |
} | |
diff = max -min; | |
for(let i = 0; i<=daysToShow ;i++){ | |
let hourData = highTemps[i]; | |
let nextHourTemp = shouldRound(roundedGraph, highTemps[i+1]); | |
let hour = parseInt[i]; | |
let temp = highTemps[i]; | |
let delta = (diff>0)?(shouldRound(roundedGraph, temp) - min) / diff:0.5; | |
let nextDelta = (diff>0)?(nextHourTemp - min) / diff:0.5 | |
if(i < daysToShow) | |
drawLine(spaceBetweenDays * (i) + 50, 215 - (50 * delta),spaceBetweenDays * (i+1) + 50 , 215 - (50 * nextDelta), 4, accentColor) | |
drawTextC(shouldRound(roundedTemp, temp)+"°", 23, spaceBetweenDays*i+27, 145 - (50*delta), 50, 23, Color.white()) | |
if (i < 1) { | |
drawText(symbolForCondition(restOfDay), 35, spaceBetweenDays * i + 30, 192 - (50*delta), Color.white()) | |
drawTextC('Now', 18, spaceBetweenDays*i+23, 230,50, 21, Color.white()); | |
if (restOfDayChance > .09) { | |
let percentChance = Math.round(restOfDayChance * 100) | |
let percentRepresent = (percentChance.toString() + '%'); | |
drawTextC(percentRepresent, 18, spaceBetweenDays*i+23, 250, 50, 21, Color.white()); | |
} | |
} else { | |
drawText(symbolForCondition(dailyConditions[i]), 35, spaceBetweenDays * i + 30, 192 - (50*delta), Color.white()) | |
let todayDate = new Date(forecastDates[i]); | |
let todayDay = todayDate.toLocaleDateString('en-us', {weekday:"short"}); | |
drawTextC(todayDay, 18, spaceBetweenDays*i+23, 230,50, 21, Color.white()); | |
if(precipChance[i] > .09) { | |
let percentChance = Math.round(precipChance[i] * 100) | |
let percentRepresent = (percentChance.toString() + '%'); | |
drawTextC(percentRepresent, 18, spaceBetweenDays*i+23, 250,50, 21, Color.white()); | |
} | |
} | |
previousDelta = delta; | |
} | |
if (data.dailydelta > 0) { | |
var dld = (data.dailydelta + '° warmer today') | |
} else if (data.dailydelta < 0) { | |
var dld = (Math.abs(data.dailydelta) + '° cooler today') | |
} else { | |
var dld = '' | |
} | |
drawText(dld, footerFontSize, todayTempChangeCoords.x, todayTempChangeCoords.y, accentColor) | |
if (data.hourdelta > 0.9) { | |
var hrd = (Math.round(data.hourdelta) + '° warmer last hour') | |
} else if (data.hourdelta < -0.9) { | |
var hrd = (Math.round(Math.abs(data.hourdelta)) + '° cooler last hour') | |
} | |
else { | |
var hrd = '' | |
} | |
drawContext.setTextAlignedRight(); | |
drawTextC(hrd, footerFontSize, (hourlyDeltaTimePosAndSize.x - 155), hourlyDeltaTimePosAndSize.y, 260, hourlyDeltaTimePosAndSize.height, accentColor) | |
widget.backgroundImage = (drawContext.getImage()) | |
return widget | |
function drawText(text, fontSize, x, y, color = Color.black()){ | |
drawContext.setFont(Font.boldSystemFont(fontSize)) | |
drawContext.setTextColor(color) | |
drawContext.drawText(new String(text).toString(), new Point(x, y)) | |
} | |
function drawImage(image, x, y){ | |
drawContext.drawImageAtPoint(image, new Point(x, y)) | |
} | |
function drawTextC(text, fontSize, x, y, w, h, color = Color.black()){ | |
drawContext.setFont(Font.boldSystemFont(fontSize)) | |
drawContext.setTextColor(color) | |
drawContext.drawTextInRect(new String(text).toString(), new Rect(x, y, w, h)) | |
} | |
function drawLine(x1, y1, x2, y2, width, color){ | |
const path = new Path() | |
path.move(new Point(x1, y1)) | |
path.addLine(new Point(x2, y2)) | |
drawContext.addPath(path) | |
drawContext.setStrokeColor(color) | |
drawContext.setLineWidth(width) | |
drawContext.strokePath() | |
} | |
function drawImage(image, x, y){ | |
drawContext.drawImageAtPoint(image, new Point(x, y)) | |
} | |
} | |
function shouldRound(should, value){ | |
return ((should) ? Math.round(value) : value) | |
} | |
function isSameDay(date1, date2){ | |
return (date1.getYear() == date2.getYear() && date1.getMonth() == date2.getMonth() && date1.getDate() == date2.getDate()) | |
} | |
function cToF(celsius) | |
{ | |
var cTemp = celsius; | |
var cToFahr = cTemp * 9 / 5 + 32; | |
return cToFahr; | |
} | |
// This function returns an word for a weather condition. | |
function wordForCondition(cond) { | |
console.log(cond); | |
// Define our symbol equivalencies. | |
let words = { | |
"Clear": function() { | |
return "Clear" | |
}, | |
"MostlyClear": function() { | |
return "Mostly Clear" | |
}, | |
"PartlyCloudy": function() { | |
return "Partly Cloudy" | |
}, | |
"MostlyCloudy": function() { | |
return "Mostly Cloudy" | |
}, | |
"Cloudy": function() { | |
return "Cloudy" | |
}, | |
"Hazy": function() { | |
return "Hazy" | |
}, | |
"Breezy": function() { | |
return "Breezy" | |
}, | |
"ScatteredThunderstorms": function() { | |
return "Thunderstorms" | |
}, | |
"thunderstorms": function() { | |
return "Thunderstorms" | |
}, | |
"Drizzle": function() { | |
return "Drizzle" | |
}, | |
"Rain": function() { | |
return "Rain" | |
}, | |
"HeavyRain": function() { | |
return "Heavy Rain" | |
} | |
} | |
// Get the symbol. | |
if( typeof words[cond] !== 'undefined' ) { | |
return words[cond]() | |
} | |
else { | |
return cond | |
} | |
} | |
// This function returns an emoji for a weather condition. | |
function symbolForCondition(cond) { | |
console.log(cond); | |
// Define our symbol equivalencies. | |
let words = { | |
"Clear": function() { | |
return "☀️" | |
}, | |
"MostlyClear": function() { | |
return "☀️" | |
}, | |
"PartlyCloudy": function() { | |
return "⛅️" | |
}, | |
"MostlyCloudy": function() { | |
return "🌥️️" | |
}, | |
"Cloudy": function() { | |
return "☁️" | |
}, | |
"Hazy": function() { | |
return "⛅️" | |
}, | |
"ScatteredThunderstorms": function() { | |
return "⛈" | |
}, | |
"Thunderstorms": function() { | |
return "⛈️" | |
}, | |
"Drizzle": function() { | |
return "🌧" | |
}, | |
"Rain": function() { | |
return "☔" | |
}, | |
"HeavyRain": function() { | |
return "☔️" | |
} | |
} | |
// Get the symbol. | |
if( typeof words[cond] !== 'undefined' ) { | |
return words[cond]() | |
} | |
else { | |
return "" | |
} | |
} | |
function createCircularWidget(temp) { | |
let tempShow = (Math.round(temp) + '°') | |
let widget = new ListWidget() | |
widget.addAccessoryWidgetBackground = true | |
let wlevel = widget.addText(tempShow) | |
wlevel.font = Font.title1() | |
wlevel.minimumScaleFactor = 0.2 | |
wlevel.centerAlignText() | |
return widget | |
} | |
Script.complete() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment