├── LICENSE ├── preview.jpg ├── readme.md └── week-forecast.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Raymond Velasquez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supermamon/scriptable-week-forecast/c07f830d2204ec448d1e356a2c9bb07e746d7584/preview.jpg -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Weekly Weather Forecast Widget 2 | 3 | A Scriptable.app widget script to display the week's weather forecast 4 | 5 | [Source](week-forecast.js) | [Import with Import-Script.js](https://open.scriptable.app/run/Import-Script?url=https://github.com/supermamon/scriptable-week-forecast/week-forecast.js) 6 | 7 | ![](preview.jpg) -------------------------------------------------------------------------------- /week-forecast.js: -------------------------------------------------------------------------------- 1 | // Variables used by Scriptable. 2 | // These must be at the very top of the file. Do not edit. 3 | // icon-color: pink; icon-glyph: sun; 4 | 5 | /* ----------------------------------------------- 6 | Script : week-forecast.js 7 | Author : dev@supermamon.com 8 | Version : 1.0.1 9 | Description : 10 | A widget script to display the week's weather 11 | forecast 12 | 13 | Requested from Reddit: 14 | https://www.reddit.com/r/Scriptable/comments/jzxgtv/is_there_any_script_for_widget_of_week_long_that/ 15 | 16 | Changelog : 17 | v1.0.1 18 | - (fix) un-uniform coloring 19 | v1.0.0 20 | - Initial release 21 | ----------------------------------------------- */ 22 | 23 | // REQUIRED!! 24 | // you need an Open Weather API key. 25 | // Get one for free at: https://home.openweathermap.org/api_keys 26 | 27 | // you can save API keys on another file so you 28 | // don't need to keep copying and pasting the 29 | // key. Bonus is you only need to change it to 30 | // on place if it changes 31 | const keys = await importModuleOptional('api-keys') 32 | 33 | // replace YOUR_API_KEY with the actual key you receive 34 | const API_KEY = keys ? keys.OpenWeatherMap : 'YOUR_API_KEY' 35 | 36 | // choose between metric, imperial, standard 37 | const UNITS = 'metric' 38 | 39 | // number of days to show in the forecast 40 | const MAX_DAYS = 5 41 | 42 | // best to use a monospace font for alignment 43 | const GLOBAL_FONT = 'Menlo-Regular' 44 | const GLOBAL_TEXT_COLOR = Color.white() 45 | 46 | // this will auto-detect location. If you wish 47 | // to provide specific location add a lat & lon 48 | // value. 49 | const weatherData = await getOpenWeatherData({appid: API_KEY, 50 | units: UNITS, 51 | reversegeocode: true 52 | }) 53 | // example with specific location 54 | /* 55 | const weatherData = await getOpenWeatherData({appid: API_KEY, 56 | units: UNITS, 57 | lat: 37.32, 58 | lon: -122.03 59 | }) 60 | */ 61 | 62 | const widget = await createWidget(weatherData.daily, config.widgetFamily) 63 | Script.setWidget(widget) 64 | Script.complete() 65 | if (config.runsInApp) { 66 | await widget.presentMedium() 67 | } 68 | 69 | //------------------------------------------------ 70 | async function createWidget(data, widgetFamily='medium') { 71 | log(`days = ${data.length}`) 72 | 73 | const fontSize = widgetFamily=='small'?9:15 74 | const useFont = new Font(GLOBAL_FONT, fontSize) 75 | 76 | 77 | const widget = new ListWidget() 78 | widget.backgroundGradient = newLinearGradient([`#277baeee`,`#277baeaa`],[0,.8]) 79 | for (var i=0; inew Color(color)) 136 | return gradient 137 | } 138 | 139 | async function importModuleOptional(module_name) { 140 | const ICLOUD = module.filename 141 | .includes('Documents/iCloud~') 142 | const fm = FileManager[ICLOUD 143 | ? 'iCloud' 144 | : 'local']() 145 | if (!/\.js$/.test(module_name)) { 146 | module_name = module_name + '.js' 147 | } 148 | const module_path = fm.joinPath 149 | (fm.documentsDirectory(), 150 | module_name) 151 | if (!fm.fileExists(module_path)) { 152 | log(`module ${module_name} does not exist`) 153 | return null 154 | } 155 | if (ICLOUD) { 156 | await fm.downloadFileFromiCloud(module_path) 157 | } 158 | const mod = importModule(module_name) 159 | return mod 160 | } 161 | 162 | //------------------------------------------------ 163 | // https://github.com/supermamon/scriptable-scripts/tree/master/openweathermap 164 | async function getOpenWeatherData({ 165 | appid='', 166 | units='metric', 167 | lang='en', 168 | exclude='minutely,alerts', 169 | revgeocode=false, 170 | ...more 171 | }) { 172 | 173 | var opts = {appid, units, lang, exclude, revgeocode, ...more} 174 | 175 | 176 | // validate units 177 | if (!(/metric|imperial|standard/.test(opts.units))) { 178 | opts.units = 'metric' 179 | } 180 | 181 | // if coordinates are not provided, attempt to 182 | // automatically find them 183 | if (!opts.lat || !opts.lon) { 184 | log('cordinates not provided. detecting...') 185 | try { 186 | var loc = await Location.current() 187 | log('successfully detected') 188 | } catch(e) { 189 | log('unable to detect') 190 | throw new Error('Unable to find your location.') 191 | } 192 | opts.lat = loc.latitude 193 | opts.lon = loc.longitude 194 | log(`located lat: ${opts.lat}, lon: ${opts.lon}`) 195 | } 196 | 197 | // ready to fetch the weather data 198 | let url = `https://api.openweathermap.org/data/2.5/onecall?lat=${opts.lat}&lon=${opts.lon}&exclude=${opts.exclude}&units=${opts.units}&lang${opts.lat}&appid=${opts.appid}` 199 | let req = new Request(url) 200 | let wttr = await req.loadJSON() 201 | if (wttr.cod) { 202 | throw new Error(wttr.message) 203 | } 204 | 205 | // add some information not provided by OWM 206 | wttr.tempUnit = opts.units == 'metric' ? 207 | 'C' : 'F' 208 | 209 | const currUnits = { 210 | standard: { 211 | temp: "K", 212 | pressure: "hPa", 213 | visibility: "m", 214 | wind_speed: "m/s", 215 | wind_gust: "m/s", 216 | rain: "mm", 217 | snow: "mm" 218 | } , 219 | metric: { 220 | temp: "C", 221 | pressure: "hPa", 222 | visibility: "m", 223 | wind_speed: "m/s", 224 | wind_gust: "m/s", 225 | rain: "mm", 226 | snow: "mm" 227 | }, 228 | imperial: { 229 | temp: "F", 230 | pressure: "hPa", 231 | visibility: "m", 232 | wind_speed: "mi/h", 233 | wind_gust: "mi/h", 234 | rain: "mm", 235 | snow: "mm" 236 | } 237 | } 238 | 239 | wttr.units = currUnits[opts.units] 240 | 241 | if (opts.revgeocode) { 242 | log('reverse geocoding...') 243 | var geo = await Location.reverseGeocode(opts.lat, opts.lon) 244 | if (geo.length) { 245 | wttr.geo = geo[0] 246 | } 247 | } 248 | 249 | //---------------------------------------------- 250 | // SFSymbol function 251 | // Credits to @eqsOne | https://talk.automators.fm/t/widget-examples/7994/414 252 | // Reference: https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2 253 | const symbolForCondition = function(cond,night=false){ 254 | let symbols = { 255 | // Thunderstorm 256 | "2": function(){ 257 | return "cloud.bolt.rain.fill" 258 | }, 259 | // Drizzle 260 | "3": function(){ 261 | return "cloud.drizzle.fill" 262 | }, 263 | // Rain 264 | "5": function(){ 265 | return (cond == 511) ? "cloud.sleet.fill" : "cloud.rain.fill" 266 | }, 267 | // Snow 268 | "6": function(){ 269 | return (cond >= 611 && cond <= 613) ? "cloud.snow.fill" : "snow" 270 | }, 271 | // Atmosphere 272 | "7": function(){ 273 | if (cond == 781) { return "tornado" } 274 | if (cond == 701 || cond == 741) { return "cloud.fog.fill" } 275 | return night ? "cloud.fog.fill" : "sun.haze.fill" 276 | }, 277 | // Clear and clouds 278 | "8": function(){ 279 | if (cond == 800) { return night ? "moon.stars.fill" : "sun.max.fill" } 280 | if (cond == 802 || cond == 803) { return night ? "cloud.moon.fill" : "cloud.sun.fill" } 281 | return "cloud.fill" 282 | } 283 | } 284 | // Get first condition digit. 285 | let conditionDigit = Math.floor(cond / 100) 286 | return symbols[conditionDigit]() 287 | 288 | } 289 | 290 | // find the day that matched the epoch `dt` 291 | var findDay = function(dt) { 292 | return wttr.daily.filter( daily => { 293 | var hDate = new Date( 1000 * dt ) 294 | var dDate = new Date( 1000 * daily.dt ) 295 | return ( 296 | hDate.getYear() == dDate.getYear() && 297 | hDate.getMonth() == dDate.getMonth() && 298 | hDate.getDate() == dDate.getDate()) 299 | })[0] 300 | } 301 | 302 | // tell whether it's night or day 303 | var day = findDay(wttr.current.dt) 304 | 305 | wttr.current.is_night = ( 306 | wttr.current.dt > day.sunset || 307 | wttr.current.dt < day.sunrise) 308 | 309 | wttr.current.weather[0].sfsymbol = 310 | symbolForCondition( 311 | wttr.current.weather[0].id, 312 | wttr.current.is_night) 313 | 314 | let wicon = wttr.current.weather[0].icon 315 | wttr.current.weather[0].icon_url = 316 | `http://openweathermap.org/img/wn/@2x.png${wicon}` 317 | 318 | wttr.hourly.map( hourly => { 319 | 320 | var day = findDay(hourly.dt) 321 | hourly.is_night = ( 322 | hourly.dt > day.sunset || 323 | hourly.dt < day.sunrise) 324 | 325 | hourly.weather[0].sfsymbol = 326 | symbolForCondition( 327 | hourly.weather[0].id, 328 | hourly.is_night) 329 | 330 | let wicon = hourly.weather[0].icon 331 | hourly.weather[0].icon_url = 332 | `http://openweathermap.org/img/wn/@2x.png${wicon}` 333 | 334 | return hourly 335 | }) 336 | 337 | wttr.daily.map( daily => { 338 | 339 | daily.weather[0].sfsymbol = 340 | symbolForCondition( 341 | daily.weather[0].id, 342 | false) 343 | 344 | let wicon = daily.weather[0].icon 345 | daily.weather[0].icon_url = 346 | `http://openweathermap.org/img/wn/@2x.png${wicon}` 347 | 348 | return daily 349 | }) 350 | 351 | 352 | // also return the arguments provided 353 | wttr.args = opts 354 | 355 | //log(wttr) 356 | return wttr 357 | 358 | } --------------------------------------------------------------------------------