This flow fetches weather data for a given location from openweathermap.org (you need a free API key) and crams it into a 9 x 4 unit UI template.
Features:
- Forecasts are shown hourly for up to 6 hours plus today and the next 4 days.
- Times (and days - Auckland) are the local time, not UTC
- The background colour blends from cool blue to warm red according to the "Feels Like" temperature.
- If there are weather alerts the text is shown and icons change colour for the affected period (Tobermory).
- Required inputs are Location name, latitude & longitude so it should be possible to pick from a map.
- Optional inputs are subjective cold and hot according to the location (a Greenlander probably feels hot at a lower temperature than a Sri Lankan). Obviously not so easy to derive from a map.
- The template is HTML and CSS only.
Issues:
- It's cramped. Very small text.
- If both rain and snow amounts are predicted, the snow amount is very cramped (Tobermory).
- But if neither, there is a noticeable gap below the icons (N'Djamena).
- Sometimes a weather alert is shown which relates to a day beyond the 5 days shown (Juneau).
- Weather alerts are in the local language? May cause display issues?
Would be nice?
- A local time digital clock.
- "Take an umbrella" notication.
- UV index.
[{"id":"80504545190c9b4d","type":"tab","label":"Weather Demo","disabled":false,"info":"","env":[]},{"id":"1d9a627667e3b8fc","type":"http request","z":"80504545190c9b4d","name":"Openweathermap","method":"GET","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","authType":"","senderr":false,"x":130,"y":180,"wires":[["cf1bc982dc9e353c"]]},{"id":"cf1bc982dc9e353c","type":"change","z":"80504545190c9b4d","name":"Temp for background colour","rules":[{"t":"set","p":"fudge","pt":"msg","to":"payload.current.feels_like","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":380,"y":180,"wires":[["1b6f730c6ab2223f"]]},{"id":"06c1eecd02273e6a","type":"ui_dropdown","z":"80504545190c9b4d","name":"Available locations","label":"","tooltip":"","place":"Select option","group":"0b2c5ed94dd5b6dc","order":8,"width":0,"height":0,"passthru":true,"multiple":false,"options":[{"label":"Trincomalee, Sri Lanka","value":"Trincomalee","type":"str"},{"label":"Juneau, Alaska","value":"Juneau","type":"str"},{"label":"Tobermory, Scotland","value":"Tobermory","type":"str"},{"label":"Qaqortaq, Greenland","value":"Qaqortaq","type":"str"},{"label":"N'Djamena, Chad","value":"N'Djamena","type":"str"},{"label":"Bangui. Central African Republic","value":"Bangui","type":"str"},{"label":"Auckland. New Zealand","value":"Auckland","type":"str"}],"payload":"","topic":"topic","topicType":"msg","className":"","x":130,"y":120,"wires":[["e01860f95dea2e98"]]},{"id":"e01860f95dea2e98","type":"function","z":"80504545190c9b4d","name":"Setup URL & location","func":"const appid = global.get('openweathermapid'); // Openweathermap API key\n\nconst hot = 32; // default \"hot\" temperature. Above this the background colour is 100% layer 2\nconst cold= 12; // default \"cold\" temperature. Below this the background colour is 100% Background\n\nconst locations = {\n\"Tobermory\": {lat: 56.622149, long: -6.069254, coldat:7, hotat:18}, //Good for weather alerts\n\"Trincomalee\": {lat: 8.647208, long: 81.177637}, //coldat and hotat unspecified\n\"Juneau\": {lat: 58.357577, long: -134.569945, coldat:0, hotat:15}, //Good for weather alerts, checking day names\n\"Qaqortaq\": {lat: 60.71839, long: -46.03561, coldat:-5, hotat:5}, //Reliably chilly\n\"N'Djamena\": {lat: 12.10672, long: 15.0444, coldat:20, hotat:30}, //Reliably warm\n\"Bangui\": {lat: 4.366667, long: 18.583333, coldat:20, hotat:30}, //Reliably warm\n\"Auckland\": {lat: -36.848461, long: 174.763336, coldat:10, hotat:25}, //Good for checking day names\n};\n\n\nmsg = {\n location: msg.payload, // Location name for display\n lat: locations[msg.payload].lat, // Must have\n long: locations[msg.payload].long, // Must have\n coldat: (locations[msg.payload].coldat || cold), // Subjective \"it feels hot/cold\" levels for location\n hotat: (locations[msg.payload].hotat || hot ),\n alertcolor: \"chocolate\", // Icons this colour if there is a weather alert\n}\n\nmsg.url=\"https://api.openweathermap.org/data/2.5/onecall?lat=\" + msg.lat + \"&lon=\" + msg.long + \"&units=metric&appid=\" + appid;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":360,"y":120,"wires":[["1d9a627667e3b8fc"]]},{"id":"2c4e9abec36c3fb6","type":"ui_template","z":"80504545190c9b4d","group":"0b2c5ed94dd5b6dc","name":"Weather Widget","order":2,"width":"9","height":"4","format":"<div class=\"weatherwidget rounded\" style=\"--fudge: {{msg.fudge}}; --coldat: {{msg.coldat}}; --hotat: {{msg.hotat}}\">\n <div class=\"layer1 rounded\" style=\"--fudge: {{msg.fudge}}; --coldat: {{msg.coldat}}; --hotat: {{msg.hotat}}\">\n <div class=\"layer2 rounded bordered\" style=\"--fudge: {{msg.fudge}}; --coldat: {{msg.coldat}}; --hotat: {{msg.hotat}}\">\n <div class=\"location\">\n <span class=\"location\">{{(msg.location || \"no data\")}}</span>\n </div>\n <div class=\"temperature\">\n <div class=\"actual\">\n <div class=\"value\">{{msg.payload.current.temp | number:1}}°C</div>\n </div>\n <div class=\"feelslike\">\n <div class=\"value\">({{msg.payload.current.feels_like |number:0}}°)</div>\n </div>\n </div>\n <div class=\"currentwrapper\">\n <div class=\"current\">\n <div class=\"wind\">\n <span>Wind: {{msg.payload.current.wind_knots | number:0}}kt {{msg.payload.current.wind_point}}</span>\n </div>\n <div class=\"pressure\">\n <span>Press: {{msg.payload.current.pressure}}hPa</span>\n </div>\n <div class=\"humidity\">\n <span>Hum: {{msg.payload.current.humidity}}%</span>\n </div>\n </div>\n </div>\n \n <div class=\"comments\">{{msg.comments}}</div>\n \n <div class=\"iconswrapper\">\n <div class=\"hourly\">\n <div class=\"icon\">\n <div class=\"subtitle time\">{{msg.payload.hourly[1].localtime}}</div>\n <i class=\"wi wi-owm-{{msg.payload.hourly[1].weather[0].icon}}\" style=\"{{msg.payload.hourly[1].iconstyle}}\"></i>\n <div class=\"subtitle temp\">{{msg.payload.hourly[1].temp | number:0}}°</div>\n <div class=\"subtitle rain\">{{msg.payload.hourly[1].rainmm}}</div>\n <div class=\"subtitle snow\">{{msg.payload.hourly[1].snowmm}}</div>\n </div>\n <div class=\"icon\">\n <div class=\"subtitle time\">{{msg.payload.hourly[2].localtime}}</div>\n <i class=\"wi wi-owm-{{msg.payload.hourly[2].weather[0].icon}}\" style=\"{{msg.payload.hourly[2].iconstyle}}\"></i>\n <div class=\"subtitle temp\">{{msg.payload.hourly[2].temp | number:0}}°</div>\n <div class=\"subtitle rain\">{{msg.payload.hourly[2].rainmm}}</div>\n <div class=\"subtitle snow\">{{msg.payload.hourly[4].snowmm}}</div>\n </div>\n <div class=\"icon\">\n <div class=\"subtitle time\">{{msg.payload.hourly[3].localtime}}</div>\n <i class=\"wi wi-owm-{{msg.payload.hourly[3].weather[0].icon}}\" style=\"{{msg.payload.hourly[3].iconstyle}}\"></i>\n <div class=\"subtitle temp\">{{msg.payload.hourly[3].temp | number:0}}°</div>\n <div class=\"subtitle rain\">{{msg.payload.hourly[3].rainmm}}</div>\n <div class=\"subtitle snow\">{{msg.payload.hourly[3].snowmm}}</div>\n </div>\n <div class=\"icon\">\n <div class=\"subtitle time\">{{msg.payload.hourly[4].localtime}}</div>\n <i class=\"wi wi-owm-{{msg.payload.hourly[4].weather[0].icon}}\" style=\"{{msg.payload.hourly[4].iconstyle}}\"></i>\n <div class=\"subtitle temp\">{{msg.payload.hourly[4].temp | number:0}}°</div>\n <div class=\"subtitle rain\">{{msg.payload.hourly[4].rainmm}}</div>\n <div class=\"subtitle snow\">{{msg.payload.hourly[4].snowmm}}</div>\n </div>\n <div class=\"icon\">\n <div class=\"subtitle time\">{{msg.payload.hourly[5].localtime}}</div>\n <i class=\"wi wi-owm-{{msg.payload.hourly[5].weather[0].icon}}\" style=\"{{msg.payload.hourly[5].iconstyle}}\"></i>\n <div class=\"subtitle temp\">{{msg.payload.hourly[5].temp | number:0}}°</div>\n <div class=\"subtitle rain\">{{msg.payload.hourly[5].rainmm}}</div>\n <div class=\"subtitle snow\">{{msg.payload.hourly[5].snowmm}}</div>\n </div>\n <div class=\"icon\">\n <div class=\"subtitle time\">{{msg.payload.hourly[6].localtime}}</div>\n <i class=\"wi wi-owm-{{msg.payload.hourly[6].weather[0].icon}}\" style=\"{{msg.payload.hourly[6].iconstyle}}\"></i>\n <div class=\"subtitle temp\">{{msg.payload.hourly[6].temp | number:0}}°</div>\n <div class=\"subtitle rain\">{{msg.payload.hourly[6].rainmm}}</div>\n <div class=\"subtitle snow\">{{msg.payload.hourly[6].snowmm}}</div>\n </div>\n </div>\n \n <div class=\"daily\">\n <div class=\"icon\">\n <div class=\"subtitle time\">{{msg.payload.daily[0].day}}</div>\n <i class=\"wi wi-owm-{{msg.payload.daily[0].weather[0].icon}}\" style=\"{{msg.payload.daily[1].iconstyle}}\"></i>\n <div class=\"subtitle temp\">{{msg.payload.daily[0].temp.max | number:0}}°</div>\n <div class=\"subtitle rain\">{{msg.payload.daily[0].rainmm}}</div>\n <div class=\"subtitle snow\">{{msg.payload.daily[0].snowmm}}</div>\n </div>\n <div class=\"icon\">\n <div class=\"subtitle time\">{{msg.payload.daily[1].day}}</div>\n <i class=\"wi wi-owm-{{msg.payload.daily[1].weather[0].icon}}\" style=\"{{msg.payload.daily[1].iconstyle}}\"></i>\n <div class=\"subtitle temp\">{{msg.payload.daily[1].temp.max | number:0}}°</div>\n <div class=\"subtitle rain\">{{msg.payload.daily[1].rainmm}}</div>\n <div class=\"subtitle snow\">{{msg.payload.daily[1].snowmm}}</div>\n </div>\n <div class=\"icon\">\n <div class=\"subtitle time\">{{msg.payload.daily[2].day}}</div>\n <i class=\"wi wi-owm-{{msg.payload.daily[2].weather[0].icon}}\" style=\"{{msg.payload.daily[1].iconstyle}}\"></i>\n <div class=\"subtitle temp\">{{msg.payload.daily[2].temp.max | number:0}}°</div>\n <div class=\"subtitle rain\">{{msg.payload.daily[2].rainmm}}</div>\n <div class=\"subtitle snow\">{{msg.payload.daily[2].snowmm}}</div>\n </div> \n <div class=\"icon\">\n <div class=\"subtitle time\">{{msg.payload.daily[3].day}}</div>\n <i class=\"wi wi-owm-{{msg.payload.daily[3].weather[0].icon}}\" style=\"{{msg.payload.daily[1].iconstyle}}\"></i>\n <div class=\"subtitle temp\">{{msg.payload.daily[3].temp.max | number:0}}°</div>\n <div class=\"subtitle rain\">{{msg.payload.daily[3].rainmm}}</div>\n <div class=\"subtitle snow\">{{msg.payload.daily[3].snowmm}}</div>\n </div>\n <div class=\"icon\">\n <div class=\"subtitle time\">{{msg.payload.daily[4].day}}</div>\n <i class=\"wi wi-owm-{{msg.payload.daily[4].weather[0].icon}}\" style=\"{{msg.payload.daily[1].iconstyle}}\"></i>\n <div class=\"subtitle temp\">{{msg.payload.daily[4].temp.max | number:0}}°</div>\n <div class=\"subtitle rain\">{{msg.payload.daily[4].rainmm}}</div>\n <div class=\"subtitle snow\">{{msg.payload.daily[4].snowmm}}</div>\n </div>\n </div>\n </div>\n </div>\n </div>\n <div class=\"copyright\"><span>Weather data from openweathermap.org</span></div>\n</div>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":800,"y":180,"wires":[[]]},{"id":"1b6f730c6ab2223f","type":"function","z":"80504545190c9b4d","name":"Adjust output","func":"const alertstyle = \"color: var(--alertcolor)\";\nmsg.debug = {location: msg.location, alerts:{}, days:\"\", hours:\"\"};\nfunction dayName(unixTime){\n var dateObject = new Date(unixTime * 1000);\n return dateObject.toLocaleString(\"default\", { weekday: \"short\" })\n}\nfunction timeConvert(UNIX_timestamp){\n var dateObject = new Date(UNIX_timestamp * 1000);\n return dateObject.toLocaleString('en', { timezone: msg.payload.timezone, hour12: false, hour: 'numeric'}) + ':00';\n}\nfunction degreesToCompass(deg){\n if (deg>11.25 && deg<=33.75){return \"NNE\";}\n else if (deg<=56.25){return \"NE\";}\n else if (deg<=78.75){return \"ENE\";}\n else if (deg<=101.25){return \"E\";}\n else if (deg<=123.75){return \"ESE\";}\n else if (deg<=146.25){return \"SE\";}\n else if (deg<=168.75){return \"SSE\";}\n else if (deg<=191.25){return \"S\";}\n else if (deg<=213.75){return \"SSW\";}\n else if (deg<=236.25){return \"SW\";}\n else if (deg<=258.75){return \"WSW\";}\n else if (deg<=281.25){return \"W\";}\n else if (deg<=303.75){return \"WNW\";}\n else if (deg<=326.25){return \"NW\";}\n else if (deg<=348.75){return \"NNW\";}\n else {return \"N\";}\n}\n\n// wind as compass points \nmsg.payload.current.wind_point = degreesToCompass(msg.payload.current.wind_deg);\nmsg.payload.current.wind_knots = msg.payload.current.wind_speed * 1.944;\nmsg.payload.current.wind_mph = msg.payload.current.wind_speed * 2.237;\n\n// Weather alerts\nif (msg.payload.hasOwnProperty('alerts')) { // There may be several weather alerts but only space to show one\n msg.comments = msg.payload.alerts[0].event; // No good way to pick the most urgent\n}\n\n// Get rain & snow qty and alerts for each hour, Adjust display to show local time\n// Not sure if browser timezone is relevant?\nmsg.payload.hourly.forEach(element => {\n const adjustedtime = element.dt + msg.payload.timezone_offset;\n element.localtime = timeConvert(adjustedtime); \n element.day = dayName(adjustedtime);\n if (element.hasOwnProperty('rain')) {element.rainmm = Math.round(element.rain[\"1h\"]) + \"mm\";}\n if (element.hasOwnProperty('snow')) {element.snowmm = Math.round(element.snow[\"1h\"]) + \"mm\";}\n if (msg.payload.hasOwnProperty('alerts')) {\n msg.payload.alerts.forEach(alert => {\n if (element.dt <= alert.end && (element.dt + 3600) >= alert.start) {\n element.warning = alert.event;\n element.iconstyle = alertstyle;\n }\n });\n }\n});\n\n// Get rain & snow qty and alerts for each day\nmsg.payload.daily.forEach(element => {\n const adjustedtime = element.dt + msg.payload.timezone_offset;\n element.localtime = timeConvert(adjustedtime);\n element.day = dayName(adjustedtime);\n if (element.hasOwnProperty('rain')) {\n element.rainmm = Math.round(element.rain) + \"mm\";\n }\n if (element.hasOwnProperty('snow')) {\n element.snowmm = Math.round(element.snow) + \"mm\";\n }\n if (msg.payload.hasOwnProperty('alerts')) {\n msg.payload.alerts.forEach(alert => {\n if (element.dt <= alert.end && (element.dt + 86400) >= alert.start) {\n element.warning = alert.event;\n element.iconstyle = alertstyle;\n }\n });\n }\n});\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":610,"y":180,"wires":[["2c4e9abec36c3fb6"]]},{"id":"953c086699766456","type":"ui_template","z":"80504545190c9b4d","group":"0b2c5ed94dd5b6dc","name":"CSS","order":3,"width":"0","height":"0","format":"<style>\n:root {\n/* these are the colours of the three layers */\n--background: 126,219,251;\n--r1: 250; --g1: 250; --b1: 147;\n--r2: 255; --g2: 179; --b2: 179;\n/* and text */\n--textcolor: black;\n--alertcolor: chocolate; \n/* You can adjust the blending of colours with this. Suggest 0.4 - 0.6 */\n--blend: 0.5;\n}\n.weatherwidget{\n position: relative;\n height: calc(100% - 8px);\n width: 100%;\n background-color: rgb(var(--background));\n}\n.weatherwidget .location {\n position: absolute;\n top: 0px;\n left: 5px;\n width: 100%;\n font-family: \"Times New Roman\", serif;\n font-size: 42px;\n color: var(--textcolor);\n}\n.weatherwidget .temperature {\n position: absolute;\n margin-left: 10px;\n top: 42px;\n width: 93%;\n height: 80%;\n color: var(--textcolor);\n font-family: calibri, sans-serif;\n}\n.weatherwidget .temperature .actual {\n position: relative;\n width: 120px; \n margin-left: 0px;\n min-height: 40px;\n float:left;\n top: 0px;\n}\n.weatherwidget .temperature .actual .value {\n position: absolute;\n top:0px;\n left:0px;\n font-size: 40px;\n}\n.weatherwidget .temperature .feelslike {\n position:relative;\n width: 80px;\n margin-left: 0px;\n height: 70px;\n float: left;\n top:0px;\n}\n.weatherwidget .temperature .feelslike .value {\n position: absolute;\n top: 3px;\n left:0px;\n font-size: 32px;\n}\n\n.weatherwidget .currentwrapper {\n position: absolute;\n width: 180px;\n right: 10px;\n top: 0px;\n}\n.weatherwidget .current {\n position: relative;\n color: var(--textcolor);\n font-size: 16px;\n font-family: verdana, sans-serif;\n}\n.weatherwidget .current .wind {\n position: absolute;\n top: 8px;\n right: 0px;\n}\n.weatherwidget .current .pressure {\n position: absolute;\n top: 26px;\n right:0px;\n}\n.weatherwidget .current .humidity {\n position: absolute;\n top: 44px;\n right: 0px;\n}\n\n.weatherwidget .comments {\n position: absolute;\n font-size: 18px;\n right:10px;\n top: 64px;\n color: var(--alertcolor);\n font-family: verdana, sans-serif;\n}\n\n.weatherwidget .iconswrapper {\n position: absolute;\n top: 92px;\n left: 10px;\n width: calc(100% - 10px);\n color: var(--textcolor);\n text-align: center;\n font-family: verdana, sans-serif;\n}\n.weatherwidget .iconswrapper .hourly {\n position: relative;\n top: 0px;\n float:left;\n}\n.weatherwidget .iconswrapper .daily {\n position: relative;\n top: 0px;\n float: right;\n}\n.weatherwidget .iconswrapper .icon {\n float: left;\n min-width: 40px;\n}\n.weatherwidget .iconswrapper .icon i {\n padding-bottom: 5px;\n font-size: 26px; /* using fa-2x instead makes icon slightly too wide */\n}\n.weatherwidget .iconswrapper .subtitle {\n font-size: 11px;\n margin-bottom: 2px;\n}\n.weatherwidget .iconswrapper .subtitle.time {\n margin-bottom: 5px;\n}\n.weatherwidget .iconswrapper .subtitle.rain {\n color: blue;\n}\n.weatherwidget .iconswrapper .subtitle.snow {\n color: white;\n}\n.bordered{\n box-sizing: border-box;\n border:4px solid black;\n margin-bottom: 2px;\n}\n.rounded {\n border-radius: 10px;\n}\n.weatherwidget .layer1 {\n--r: var(--r1);\n--g: var(--g1);\n--b: var(--b1);\n--start:var(--coldat);\n}\n.weatherwidget .layer2 {\n--r: var(--r2);\n--g: var(--g2);\n--b: var(--b2);\n--start:calc(var(--hotat) - (var(--span)));\n}\n.weatherwidget .layer2, .weatherwidget .layer1 {\n--span: calc((var(--hotat) - var(--coldat)) * var(--blend));\n position: absolute;\n width: 100%;\n height: 100%;\n top: 0;\n box-shadow: 4px 4px 28px 0px rgba(0,0,0,0.3) inset;\n background-color: rgba(var(--r), var(--g), var(--b), calc((var(--fudge) - var(--start)) / var(--span)));\n}\n.weatherwidget .copyright {\n position: absolute;\n bottom: -10px;\n right: 10px;\n font-size:10px;\n font-family: verdana, sans-serif;\n max-height: 15px;\n}\n</style>\n","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":770,"y":120,"wires":[[]]},{"id":"0b2c5ed94dd5b6dc","type":"ui_group","name":"Demo","tab":"4311353dbffb7643","order":1,"disp":true,"width":"10","collapse":false,"className":""},{"id":"4311353dbffb7643","type":"ui_tab","name":"Demo","icon":"dashboard","disabled":false,"hidden":false}]