Skip to content

Instantly share code, notes, and snippets.

@jasonsnell
Created July 4, 2024 19:45
Show Gist options
  • Save jasonsnell/239f8d8c3cfc113d7461d57a8887ea79 to your computer and use it in GitHub Desktop.
Save jasonsnell/239f8d8c3cfc113d7461d57a8887ea79 to your computer and use it in GitHub Desktop.
Weather graph widget for Scriptable
// 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