Compact Multi-location Weather Display

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}]
7 Likes

Just don't eat the snow in Tobermory :rofl:

Why, what did you do? :stuck_out_tongue_winking_eye:

Apparently its Yellow :nauseated_face:

2 Likes

Womble spoor.

1 Like