Skip to content

Instantly share code, notes, and snippets.

@giuliomagnifico
Last active February 5, 2024 10:34
Show Gist options
  • Save giuliomagnifico/efd3ecd628a96d714e840c98ac77463f to your computer and use it in GitHub Desktop.
Save giuliomagnifico/efd3ecd628a96d714e840c98ac77463f to your computer and use it in GitHub Desktop.
WeatherLine full
// 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" : "46.062" , "LON" : "13.242" , "LOC_NAME" : "Udine" }')
// 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 = "your_key_here"
// 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).
// Hardcoded Location, type in your latitude/longitude values and location name
var LAT = widgetParams.LAT // 12.34
var LON = widgetParams.LON // 12.34
var LOCATION_NAME = widgetParams.LOC_NAME // "Your place"
// 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.
// Support locales
const locale = "en" // "fr" "it" "de" etc. for weather description language
const nowstring = "now" // Your local term for "now"
const feelsstring = "" //Your local term for "feels like"
const relHumidity = "" // any local term for "humidity"
const pressure = ""
const windspeed = ""
// 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 = false
// 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, 26)
// locationNameFontSize : number > Size in pixels of the font of the location label.
const locationNameFontSize = 26
// 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 = 18
//feelsLikeCoords : Point > Coordinates of the "feels like" label.
const feelsLikeCoords = new Point(28, 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. **/
// Set up cache. File located in the Scriptable iCloud folder
let fm = FileManager.iCloud();
let cachePath = fm.joinPath(fm.documentsDirectory(), "weatherCache"); // <- file name
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=" + locale + "&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;
}
}
// 'Night' boolean for line graph and SFSymbols
var night = (hourData.dt > hourDay.sunset || hourData.dt < hourDay.sunrise)
drawLine(spaceBetweenDays * (i) + 50, 175 - (50 * delta),spaceBetweenDays * (i+1) + 50 , 175 - (50 * nextDelta), 4, (night ? Color.gray() : accentColor))
}
drawTextC(shouldRound(roundedTemp, temp)+"°", 20, spaceBetweenDays*i+30, 135 - (50*delta), 50, 21, Color.white())
// Next 2 lines SFSymbols tweak
const condition = i==0?weatherData.current.weather[0].id:hourData.weather[0].id
drawImage(symbolForCondition(condition), spaceBetweenDays * i + 34, 161 - (50*delta)); //40, 165
drawTextC((i==0?nowstring:hour), 18, spaceBetweenDays*i+25, 200,50, 21, Color.gray())
previousDelta = delta;
}
drawText(feelsstring + " " + Math.round(weatherData.current.feels_like) + "° | " + relHumidity + " " + weatherData.current.humidity + "% | " + pressure + " " + weatherData.current.pressure + "hPa | " + windspeed + " " + weatherData.current.wind_speed + "m/s", footerFontSize, feelsLikeCoords.x, feelsLikeCoords.y, Color.gray())
drawContext.setTextAlignedRight();
drawTextC(epochToDate(weatherData.current.dt).toLocaleTimeString('fr-FR', {hour: '2-digit', minute: '2-digit'} ), footerFontSize, lastUpdateTimePosAndSize.x, lastUpdateTimePosAndSize.y, lastUpdateTimePosAndSize.width, lastUpdateTimePosAndSize.height, Color.gray())
if(usingCachedData)
drawText("⚠️", 32, ((config.widgetFamily == "small") ? contextSize : mediumWidgetWidth)-72,30)
widget.backgroundImage = (drawContext.getImage())
widget.presentMedium()
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())
}
// SFSymbol function
function symbolForCondition(cond){
let symbols = {
// Thunderstorm
"2": function(){
return "cloud.bolt.rain.fill"
},
// Drizzle
"3": function(){
return "cloud.drizzle.fill"
},
// Rain
"5": function(){
return (cond == 511) ? "cloud.sleet.fill" : "cloud.rain.fill"
},
// Snow
"6": function(){
return (cond >= 611 && cond <= 613) ? "cloud.snow.fill" : "snow"
},
// Atmosphere
"7": function(){
if (cond == 781) { return "tornado" }
if (cond == 701 || cond == 741) { return "cloud.fog.fill" }
return night ? "cloud.fog.fill" : "sun.haze.fill"
},
// Clear and clouds
"8": function(){
if (cond == 800) { return night ? "moon.stars.fill" : "sun.max.fill" }
if (cond == 802 || cond == 803) { return night ? "cloud.moon.fill" : "cloud.sun.fill" }
return "cloud.fill"
}
}
// Get first condition digit.
let conditionDigit = Math.floor(cond / 100)
// Style and return the symbol.
let sfs = SFSymbol.named(symbols[conditionDigit]())
sfs.applyFont(Font.systemFont(25))
return sfs.image
}
Script.complete()
@TheBreznsoiza
Copy link

TheBreznsoiza commented Oct 20, 2020

missing "=" in line 101
locale are not recognized from line 32 const locale = "de"

LINE 101 is wrong
weatherData = await new Request("https://api.openweathermap.org/data/2.5/onecall?lat=" + LAT + "&lon=" + LON + "&exclude=minutely,alerts&units=" + units + "&lang" + locale + "&appid=" + API_KEY).loadJSON();

should be
weatherData = await new Request("https://api.openweathermap.org/data/2.5/onecall?lat=" + LAT + "&lon=" + LON + "&exclude=minutely,alerts&units=" + units + "&lang=" + locale + "&appid=" + API_KEY).loadJSON();

@giuliomagnifico
Copy link
Author

giuliomagnifico commented Oct 20, 2020

missing "=" in line 101

Fixed, thank you!

@znellenz
Copy link

I just copied the code, entered my own API-Key and changed the widget params to munich: 48.13, 11.57. But an error occurs, that says: Error on line 8:32 SyntaxError: JSON Parse error: Unexpected EOF

@giuliomagnifico
Copy link
Author

munich: 48.13, 11.57. But an error occurs, that says: Error on line 8:32 SyntaxError: JSON Parse error: Unexpected EOF

Weird, this is a database error. Try to insert the coordinates as 3 decimal digits.

@znellenz
Copy link

Thanks for your help. Now it says: Error on line 8:32 SyntaxError: JSON Parse error: Unable to parse JSON string. And sometimes it shows me the Line of Udine...

@giuliomagnifico
Copy link
Author

giuliomagnifico commented Oct 23, 2020

I don’t know what are you doing but for me works without problems:

488F99F2-67B7-46FB-ABF8-CBBCE724CE54

Use this code

const widgetParams = JSON.parse((args.widgetParameter != null) ? args.widgetParameter : '{ "LAT" : "48.13" , "LON" : "11.57" , "LOC_NAME" : "Munich" }')

And insert your API key correctly!

@TheBreznsoiza
Copy link

@znellenz works for me as well without problems
Maybe add the whole code again fresh without changes?

@RosoV89
Copy link

RosoV89 commented Oct 24, 2020

The code doesnt work:(

Copy link

ghost commented Oct 31, 2020

And insert your API key correctly!

Great work - looks great!

I have a question about the Api- Key: Are there any risks regarding security? So what personal data is collected, are there hacker attacks or something similar? And do the tracking services on the iPhone always have to be activated for the key?

Thanks in advance for your feedback!

@giuliomagnifico
Copy link
Author

@Augustus88 No, the API key is necessary to open weather map in order to know how many queries you are making to their service. If you goes out of limit you will need to buy a pro account.

Copy link

ghost commented Oct 31, 2020

@Augustus88 No, the API key is necessary to open weather map in order to know how many queries you are making to their service. If you goes out of limit you will need to buy a pro account.

Thanks for the feedback!

Yes, I had understood that with the data transfer. But can Open Weather collect any personal data from me (e.g. location)? And I can use one key in several scripts for free, right? What exactly do you mean by "limit"?

@giuliomagnifico
Copy link
Author

giuliomagnifico commented Oct 31, 2020

I don’t know their policy about the data but this widget uses fixed coordinates, so it doesn’t changing regarding to the location you are. So OWM can know only the coordinates you have inserted, not every location you visit.

You can use a lot of widgets with the same api key but there’s a limit of queries you can make with the same api key (on a free plan), that is the purpose of the api key. Here are the pricing: https://openweathermap.org/price

Copy link

ghost commented Oct 31, 2020

Ah, okay. Thanks a lot!
If I use the free Api Key, then I don't need to pay any money? Or can it be that if the key automatically makes too many requests, I automatically have to pay something? Can I limit or adjust the queries in the widget/script?

@Mach1neCoder
Copy link

How do I change the widget to a large widget, without this breaking

@giuliomagnifico
Copy link
Author

How do I change the widget to a large widget, without this breaking

You can’t unfortunately. The widget is designed to be a medium size widget.

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