Skip to content

Instantly share code, notes, and snippets.

@ImGamez
Last active June 23, 2023 07:20
Show Gist options
  • Star 35 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save ImGamez/a8f9d77bf660d7703cc96fee87cdc4b0 to your computer and use it in GitHub Desktop.
Save ImGamez/a8f9d77bf660d7703cc96fee87cdc4b0 to your computer and use it in GitHub Desktop.
Weather widget script for Scriptable using the OpenWeather API
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: yellow; icon-glyph: cloud;
// Widget Params
// Don't edit this, those are default values for debugging (location for Cupertino).
// You need to give your locations parameters through the widget params, more info below.
const widgetParams = JSON.parse((args.widgetParameter != null) ? args.widgetParameter : '{ "LAT" : "37.32" , "LON" : "-122.03" , "LOC_NAME" : "Cupertino, US" }')
// WEATHER API PARAMETERS !important
// API KEY, you need an Open Weather API Key
// You can get one for free at: https://home.openweathermap.org/api_keys (account needed).
const API_KEY = ""
// Latitude and Longitude of the location where you get the weather of.
// You can get those from the Open Weather website while searching for a city, etc.
// This values are getted from the widget parameters, the widget parameters is a JSON string that looks like this:
// { "LAT" : "<latitude>" , "LON" : "<longitude>" , "LOC_NAME" : "<name to display>" }
// This to allow multiple instances of the widget with different locations, if you will only use one instance (1 widget), you can "hardcode" the values here.
// Note: To debug the widget you need to place the values here, because when playing the script in-app the widget parameters are null (= crash).
const LAT = widgetParams.LAT
const LON = widgetParams.LON
const LOCATION_NAME = widgetParams.LOC_NAME
// Looking settings
// This are settings to customize the looking of the widgets, because this was made an iPhone SE (2016) screen, I can't test for bigger screens.
// So feel free to modify this to your taste.
// 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 = "metric"
// twelveHours : true|false > Defines if the hours are displayed in a 12h format, use false for 24h format. (Default: true)
const twelveHours = true
// 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
// hoursToShow : number > Number of predicted hours to show, Eg: 3 = a total of 4 hours in the widget (Default: 3 for the small widget and 11 for the medium one).
const hoursToShow = (config.widgetFamily == "small") ? 3 : 11;
// 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 : 44;
// 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 = 282
// 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 = 584
// accentColor : Color > Accent color of some elements (Graph lines and the location label).
const accentColor = new Color("#EB6E4E", 1)
// backgroundColor : Color > Background color of the widgets.
const backgroundColor = new Color("#1C1C1E", 1)
// Position and size of the elements on the widget.
// All coordinates make reference to the top-left of the element.
// locationNameCoords : Point > Define the position in pixels of the location label.
const locationNameCoords = new Point(30, 30)
// locationNameFontSize : number > Size in pixels of the font of the location label.
const locationNameFontSize = 24
// weatherDescriptionCoords : Point > Position of the weather description label in pixels.
const weatherDescriptionCoords = new Point(30, 52)
// weatherDescriptionFontSize : number > Font size of the weather description label.
const weatherDescriptionFontSize = 18
//footerFontSize : number > Font size of the footer labels (feels like... and last update time).
const footerFontSize = 20
//feelsLikeCoords : Point > Coordinates of the "feels like" label.
const feelsLikeCoords = new Point(30, 230)
//lastUpdateTimePosAndSize : Rect > Defines the coordinates and size of the last updated time label.
const lastUpdateTimePosAndSize = new Rect((config.widgetFamily == "small") ? 150 : 450, 230, 100, footerFontSize+1)
//From here proceed with caution.
let fm = FileManager.iCloud();
let cachePath = fm.joinPath(fm.documentsDirectory(), "weatherCache");
if(!fm.fileExists(cachePath)){
fm.createDirectory(cachePath)
}
let weatherData;
let usingCachedData = false;
let drawContext = new DrawContext();
drawContext.size = new Size((config.widgetFamily == "small") ? contextSize : mediumWidgetWidth, contextSize)
drawContext.opaque = false
drawContext.setTextAlignedCenter()
try {
weatherData = await new Request("https://api.openweathermap.org/data/2.5/onecall?lat=" + LAT + "&lon=" + LON + "&exclude=minutely,alerts&units=" + units + "&lang=en&appid=" + API_KEY).loadJSON();
fm.writeString(fm.joinPath(cachePath, "lastread"+"_"+LAT+"_"+LON), JSON.stringify(weatherData));
}catch(e){
console.log("Offline mode")
try{
await fm.downloadFileFromiCloud(fm.joinPath(cachePath, "lastread"+"_"+LAT+"_"+LON));
let raw = fm.readString(fm.joinPath(cachePath, "lastread"+"_"+LAT+"_"+LON));
weatherData = JSON.parse(raw);
usingCachedData = true;
}catch(e2){
console.log("Error: No offline data cached")
}
}
let widget = new ListWidget();
widget.setPadding(0, 0, 0, 0);
widget.backgroundColor = backgroundColor;
drawText(LOCATION_NAME, locationNameFontSize, locationNameCoords.x, locationNameCoords.y, accentColor);
drawText(weatherData.current.weather[0].description, weatherDescriptionFontSize, weatherDescriptionCoords.x, weatherDescriptionCoords.y, Color.white())
let min, max, diff;
for(let i = 0; i<=hoursToShow ;i++){
let temp = shouldRound(roundedGraph, weatherData.hourly[i].temp);
min = (temp < min || min == undefined ? temp : min)
max = (temp > max || max == undefined ? temp : max)
}
diff = max -min;
for(let i = 0; i<=hoursToShow ;i++){
let hourData = weatherData.hourly[i];
let nextHourTemp = shouldRound(roundedGraph, weatherData.hourly[i+1].temp);
let hour = epochToDate(hourData.dt).getHours();
if(twelveHours)
hour = (hour > 12 ? hour - 12 : (hour == 0 ? "12a" : ((hour == 12) ? "12p" : hour)))
let temp = i==0?weatherData.current.temp : hourData.temp
let delta = (diff>0)?(shouldRound(roundedGraph, temp) - min) / diff:0.5;
let nextDelta = (diff>0)?(nextHourTemp - min) / diff:0.5
if(i < hoursToShow){
let hourDay = epochToDate(hourData.dt);
for(let i2 = 0 ; i2 < weatherData.daily.length ; i2++){
let day = weatherData.daily[i2];
if(isSameDay(epochToDate(day.dt), epochToDate(hourData.dt))){
hourDay = day;
break;
}
}
drawLine(spaceBetweenDays * (i) + 50, 175 - (50 * delta),spaceBetweenDays * (i+1) + 50 , 175 - (50 * nextDelta), 4, (hourData.dt > hourDay.sunset || hourData.dt < hourDay.sunrise ? Color.gray() : accentColor))
}
drawTextC(shouldRound(roundedTemp, temp)+"°", 20, spaceBetweenDays*i+30, 135 - (50*delta), 50, 21, Color.white())
drawImage(await loadImage(i==0?weatherData.current.weather[0].icon:hourData.weather[0].icon), spaceBetweenDays * i + 25, 150 - (50*delta));
drawTextC((i==0?"Now":hour), 18, spaceBetweenDays*i+25, 200,50, 21, Color.gray())
previousDelta = delta;
}
drawText("feels like " + Math.round(weatherData.current.feels_like) + "°", footerFontSize, feelsLikeCoords.x, feelsLikeCoords.y, Color.gray())
drawContext.setTextAlignedRight();
drawTextC(epochToDate(weatherData.current.dt).toLocaleTimeString(), footerFontSize, lastUpdateTimePosAndSize.x, lastUpdateTimePosAndSize.y, lastUpdateTimePosAndSize.width, lastUpdateTimePosAndSize.height, (usingCachedData) ? Color.yellow() : Color.gray())
if(usingCachedData)
drawText("⚠️", 32, ((config.widgetFamily == "small") ? contextSize : mediumWidgetWidth)-72,30)
widget.backgroundImage = (drawContext.getImage())
widget.presentMedium()
async function loadImage(imgName){
if(fm.fileExists(fm.joinPath(cachePath, imgName))){
await fm.downloadFileFromiCloud(fm.joinPath(cachePath, imgName))
return Image.fromData(Data.fromFile(fm.joinPath(cachePath, imgName)))
}else{
let imgdata = await new Request("https://openweathermap.org/img/wn/"+imgName+".png").load();
let img = Image.fromData(imgdata);
fm.write(fm.joinPath(cachePath, imgName), imgdata);
return img;
}
}
function epochToDate(epoch){
return new Date(epoch * 1000)
}
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 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())
}
Script.complete()
@bmichotte
Copy link

bmichotte commented Oct 1, 2020

Hi @ImGamez !

I made a few changes, it's not possible to create a pull request on gists... so here they are :)

+ // Support locales 
+ // Add LOCALE to the '{ "LAT" : "37.32" , "LON" : "-122.03" , "LOC_NAME" : "Cupertino, US", "LOCALE": "en" }' 
+ const LOCALE = widgetParams. LOCALE || 'en'
// .....
-   weatherData = await new Request("https://api.openweathermap.org/data/2.5/onecall?lat=" + LAT + "&lon=" + LON + "&exclude=daily,minutely,alerts&units=" + units + "&lang=en&appid=" + API_KEY).loadJSON();
+   weatherData = await new Request("https://api.openweathermap.org/data/2.5/onecall?lat=" + LAT + "&lon=" + LON + "&exclude=daily,minutely,alerts&units=" + units + "&lang=" + LOCALE + "&appid=" + API_KEY).loadJSON();
+ // show24Hours : true|false > false. Allow to hour 24 hour format
+ const show24Hours = true
// ...
+ if (!show24Hours) {
hour = (hour > 12 ? hour - 12 : (hour == 0 ? “12a” : ((hour == 12) ? “12p” : hour)))
+ }

edit: copy-paste is bad ! thanks https://talk.automators.fm/u/theguy69

@bmichotte
Copy link

Also, I've modified the lat/lng with the following code which allows you to get the lat/lng from the device

+ let latLong = {}
+ try {
+  latLong = await Location.current()
+ } catch {}

+ const LAT = latLong.latitude || widgetParams.LAT
+ const LON = latLong.longitude || widgetParams.LON
- const LAT = widgetParams.LAT
- const LON = widgetParams.LON

@Stahl80
Copy link

Stahl80 commented Nov 16, 2020

Also, I've modified the lat/lng with the following code which allows you to get the lat/lng from the device

+ let latLong = {}
+ try {
+  latLong = await Location.current()
+ } catch {}

+ const LAT = latLong.latitude || widgetParams.LAT
+ const LON = latLong.longitude || widgetParams.LON
- const LAT = widgetParams.LAT
- const LON = widgetParams.LON

Would you mind posting your complete script, I can’t get it to work with the modification? Thanks!

@PH1TCH
Copy link

PH1TCH commented Feb 24, 2021

Hi @ImGamez,

pretty cool widget, thank you for sharing 😉

I've got a small request / question: Do you think it would be possible (with only little adjustments) to only show every other hour in the widget? So e.g. the weather for 14h, 16h, 18h and so on? This could be especially useful when using the small widget size.

Thanks in advance!

@ImGamez
Copy link
Author

ImGamez commented Mar 12, 2021

Hi @PH1TCH,
Hmm, great suggestion!, of course is possible, I've never though about it 🤔, I'm going to look into it later, because I've holding the next update of the widget with a lot of new features for almost 4 months, that I want to realease first 😅. The script will jump from ~200 to ~700 lines!

Cheers! 🤙🏻

@pycckuu
Copy link

pycckuu commented Mar 17, 2021

I get error

2021-03-18 00:57:27: Error on line 108:29: TypeError: undefined is not an object (evaluating 'weatherData.current.weather')

@rwichter
Copy link

Also, I've modified the lat/lng with the following code which allows you to get the lat/lng from the device

+ let latLong = {}
+ try {
+  latLong = await Location.current()
+ } catch {}

+ const LAT = latLong.latitude || widgetParams.LAT
+ const LON = latLong.longitude || widgetParams.LON
- const LAT = widgetParams.LAT
- const LON = widgetParams.LON

This is such a clean looking widget I'm loving it on my screen :) I added a section to automatically grab your location using reverseGeocode:

+ const geocode = await Location.reverseGeocode(LAT,LONG)
+ const LOCATION_NAME = geocode[0].locality || widgetParams.LOC_NAME
- const LOCATION_NAME = widgetParams.LOC_NAME

Although sometimes this will throw an error in the widget that says in encountered an object when it expected a string. Simply rerunning the script fixes this so I'm not sure which part of the code is throwing it off... if anyone has any suggestions lmk!

@alternapop
Copy link

Also, I've modified the lat/lng with the following code which allows you to get the lat/lng from the device

+ let latLong = {}
+ try {
+  latLong = await Location.current()
+ } catch {}

+ const LAT = latLong.latitude || widgetParams.LAT
+ const LON = latLong.longitude || widgetParams.LON
- const LAT = widgetParams.LAT
- const LON = widgetParams.LON

Could you post your whole script please? I tried to make the indicated changes but it's not working.
Thank you!

@rwichter
Copy link

rwichter commented Jun 7, 2021

let latLong = {}
try {
latLong = await Location.current()
} catch {}

const LAT = latLong.latitude || widgetParams.LAT
const LON = latLong.longitude || widgetParams.LON

const geocode = await Location.reverseGeocode(LAT,LON)
const LOCATION_NAME = geocode[0].locality || widgetParams.LOC_NAME

The above replaces all the code between "// Note: To debug the widget you need to..." and "// Looking Settings"

@PH1TCH
Copy link

PH1TCH commented Jun 30, 2021

Hi @PH1TCH,
Hmm, great suggestion!, of course is possible, I've never though about it 🤔, I'm going to look into it later, because I've holding the next update of the widget with a lot of new features for almost 4 months, that I want to realease first 😅. The script will jump from ~200 to ~700 lines!

Cheers! 🤙🏻

Hi @ImGamez, are there any news about this? Is the new version ready yet? Can‘t wait 😉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment