OpenWeatherMap Weather Dashboard with Icons - Landscape

This flow pulls weather data from OpenWeatherMap (OWM) then presents it in a dashboard showing current weather conditions along with forecasts for the next 6 hours and 4 days. The mix of text and icons creates a compact and attractive display.

This version is formatted in a landscape orientation. A portrait version is also available.

image

Starting from Dashboard With Current Conditions And Forecast by djiwondee I made these changes:

One Call

I used the new One Call API from OpenWeatherMap that returns all this data:

  • Minute forecast for 1 hour
  • Hourly forecast for 48 hours
  • Daily forecast for 7 days
  • Historical data for 5 previous days

No Custom Nodes

The OpenWeatherMap custom node is good but until June 2020 it didn't support the new One Call API. So I used standard nodes, which was fairly easy. The HTTP call is simple and JSON data is returned which is perfect for Node-Red.

Weather Icons

I used the "lite" set of weather icons which are now included in Node-Red. No need to install an icon set.

Setup and Configuration

Go to OpenWeatherMap and sign up for a free account to get an API key.

Open the node labeled Settings and change these parameters:

  • Change msg.payload.lat to your latitude.
  • Change msg.payload.lon to your longitude.
  • Change msg.payload.appid to your API key.
  • Change msg.payload.units to imperial or metric.
  • Change msg.payload.lang to your language, for example en for English. List of OWM languages
  • Change msg.payload.hour12 to true to display times in 12 hour am/pm format, or false for 24 hour time format.

NOTE: You can get your latitude and longitude by looking up your town in OpenWeatherMap.

image

Changes

07 Apr 2021

  • Use timezone from OWM to compute times and dates instead of using Node Red server settings.
  • Added msg.payload.lang to support several languages. (If the short weekday names for the 4 day forecast do not match your language, you may need to upgrade to NodeJS 13 or higher when full international support was added for the toLocaleString function, or use the alternate code in the Format forecast data node.)
  • Added msg.payload.hour12 to support 12 or 24 hour time display.
  • Changed name of Set location, appid, units node to Settings.
[{"id":"5f633a28.607134","type":"tab","label":"OWM Landscape","disabled":false,"info":""},{"id":"180a101a.9cd43","type":"ui_button","z":"5f633a28.607134","name":"IconRefresh","group":"70fb931.dfc146c","order":1,"width":2,"height":2,"passthru":false,"label":"","tooltip":"Refresh","color":"","bgcolor":"","icon":"fa-refresh fa-4x","payload":"true","payloadType":"bool","topic":"","x":990,"y":240,"wires":[["c5efdf55.db32f"]]},{"id":"9c052db7.3c434","type":"ui_text","z":"5f633a28.607134","group":"70fb931.dfc146c","order":5,"width":5,"height":1,"name":"Description","label":"","format":"{{msg.payload.current.weather[0].description}}","layout":"row-left","x":990,"y":40,"wires":[]},{"id":"ec4eb2d0.974c","type":"ui_text","z":"5f633a28.607134","group":"70fb931.dfc146c","order":3,"width":3,"height":1,"name":"Wind","label":"","format":"{{msg.payload.current.wind_speed}}&nbsp;&nbsp;<i class=\"wi wi-darksky-wind\"></i>&nbsp;&nbsp;{{msg.payload.current.wind_cardinal}}","layout":"col-center","x":970,"y":120,"wires":[]},{"id":"40f44662.3ecde8","type":"ui_text","z":"5f633a28.607134","group":"70fb931.dfc146c","order":4,"width":3,"height":1,"name":"SunriseTime","label":"","format":"<i class=\"wi wi-owm-01d\"></i>&nbsp;<i class=\"fa fa-arrow-up\"></i>&nbsp;&nbsp;{{msg.payload.current.sunrise}}","layout":"row-center","x":990,"y":160,"wires":[]},{"id":"5b97953.01fdd6c","type":"ui_text","z":"5f633a28.607134","group":"70fb931.dfc146c","order":6,"width":3,"height":1,"name":"SunsetTime","label":"","format":"<i class=\"wi wi-wu-sunny\"></i>&nbsp;<i class=\"fa fa-arrow-down\"></i>&nbsp;&nbsp;{{msg.payload.current.sunset}}","layout":"row-center","x":990,"y":200,"wires":[]},{"id":"d284ed2f.c15c8","type":"comment","z":"5f633a28.607134","name":"OpenWeatherMap One Call API for Weather and Forecast","info":"","x":250,"y":40,"wires":[]},{"id":"9dbf3c95.67c74","type":"ui_template","z":"5f633a28.607134","group":"70fb931.dfc146c","name":"Forecast2","order":7,"width":10,"height":2,"format":"<div style=\"height: 100%; justify-content: center; align-items: center;\">\n <div layout=\"rowicons\" layout-align=\"space-around start\" ng-repeat=\"data in msg.payload.rowicons\" style=\"font-size:150%;padding-top: 5px;padding-bottom: 5px\">\n  <span flex style=\"color: white;text-align: center\"><i class=\"wi wi-owm-{{data.cell01}}\"></i></span>\n  <span flex style=\"color: white;text-align: center\"><i class=\"wi wi-owm-{{data.cell02}}\"></i></span>\n  <span flex style=\"color: white;text-align: center\"><i class=\"wi wi-owm-{{data.cell03}}\"></i></span>\n  <span flex style=\"color: white;text-align: center\"><i class=\"wi wi-owm-{{data.cell04}}\"></i></span>\n  <span flex style=\"color: white;text-align: center\"><i class=\"wi wi-owm-{{data.cell05}}\"></i></span>\n  <span flex style=\"color: white;text-align: center\"><i class=\"wi wi-owm-{{data.cell06}}\"></i></span>\n  <span flex style=\"color: #097479;text-align: center\"><i class=\"wi wi-owm-{{data.cell07}}\"></i></span>\n  <span flex style=\"color: #097479;text-align: center\"><i class=\"wi wi-owm-{{data.cell08}}\"></i></span>\n  <span flex style=\"color: #097479;text-align: center\"><i class=\"wi wi-owm-{{data.cell09}}\"></i></span>\n  <span flex style=\"color: #097479;text-align: center\"><i class=\"wi wi-owm-{{data.cell10}}\"></i></span>\n </div>\n <div layout=\"rowtext\" layout-align=\"space-around start\" ng-repeat=\"data in msg.payload.rowtext\" style=\"line-height: 150%\">\n  <span flex style=\"color: white;text-align: center;\">{{data.cell01}}</span>\n  <span flex style=\"color: white;text-align: center\">{{data.cell02}}</span>\n  <span flex style=\"color: white;text-align: center\">{{data.cell03}}</span>\n  <span flex style=\"color: white;text-align: center\">{{data.cell04}}</span>\n  <span flex style=\"color: white;text-align: center\">{{data.cell05}}</span>\n  <span flex style=\"color: white;text-align: center\">{{data.cell06}}</span>\n  <span flex style=\"color: #097479;text-align: center\">{{data.cell07}}</span>\n  <span flex style=\"color: #097479;text-align: center\">{{data.cell08}}</span>\n  <span flex style=\"color: #097479;text-align: center\">{{data.cell09}}</span>\n  <span flex style=\"color: #097479;text-align: center\">{{data.cell10}}</span>\n </div>\n</div>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":false,"templateScope":"local","x":980,"y":300,"wires":[[]]},{"id":"7adde26c.514aec","type":"function","z":"5f633a28.607134","name":"Format forecast data","func":"var fcdata = {};\nvar units=flow.get('units');\nif (units===undefined)\n{\n  units=\"imperial\";\n}\n\nfunction formatTemp(high, low){\n    if (units == \"imperial\") {\n        if (low){\n          temp = parseFloat(high).toFixed() + '/' + parseFloat(low).toFixed()\n        }\n        else {\n          temp = parseFloat(high).toFixed() + '°F'\n        }\n    }\n    else { // metric\n        if (low){\n          temp = parseFloat(high).toFixed() + '/' + parseFloat(low).toFixed()\n        }\n        else {\n          temp = parseFloat(high).toFixed() + '°C'\n        }\n    }\n    return temp;\n}\n\nfunction dayName(unixTime){\n  var dateObject = new Date(unixTime * 1000);\n  return dateObject.toLocaleString(flow.get('lang'), { timezone: msg.payload.timezone, weekday: 'short'});\n  // If the line above is not producing the correct short weekday names for the language set by 'lang',\n  //   you can try upgrading to NodeJS version 13 or higher (when full international support was added for the toLocaleString function),\n  //   or you can use the code below which is an example for French short weekday names.\n/*  \n  switch (dateObject.toLocaleString('en', { timezone: msg.payload.timezone, weekday: 'short'})) {\n    case 'Mon':\n      return 'Lun';\n    case 'Tue':\n      return 'Mar';\n    case 'Wed':\n      return 'Mer';\n    case 'Thu':\n      return 'Jeu';\n    case 'Fri':\n      return 'Ven';\n    case 'Sat':\n      return 'Sam';\n    case 'Sun':\n      return 'Dim';\n  }\n*/\n}\n\nfunction timeConvert(UNIX_timestamp){\n  var dateObject = new Date(UNIX_timestamp * 1000);\n  if (flow.get('hour12')) {  // 12 hour time format\n    return dateObject.toLocaleString('en', { timezone: msg.payload.timezone, hour12: true, hour: 'numeric'}).toLowerCase();\n  } else {  // 24 hour time format\n    return dateObject.toLocaleString('en', { timezone: msg.payload.timezone, hour12: false, hour: 'numeric'}) + ':00';\n  }      \n}\n\n// prepare forecast data for CSS based ui widget\nfcdata.payload = {\n  rowtext: {\n  \tdata01: {\n      cell01: timeConvert(msg.payload.hourly[1].dt),\n      cell02: timeConvert(msg.payload.hourly[2].dt),\n      cell03: timeConvert(msg.payload.hourly[3].dt),\n      cell04: timeConvert(msg.payload.hourly[4].dt),\n      cell05: timeConvert(msg.payload.hourly[5].dt),\n      cell06: timeConvert(msg.payload.hourly[6].dt),\n      cell07: dayName(msg.payload.daily[1].dt),\n      cell08: dayName(msg.payload.daily[2].dt),\n      cell09: dayName(msg.payload.daily[3].dt),\n      cell10: dayName(msg.payload.daily[4].dt),\n  \t},\n  \tdata02: {\n       cell01: formatTemp(msg.payload.hourly[1].temp),\n       cell02: formatTemp(msg.payload.hourly[2].temp),\n       cell03: formatTemp(msg.payload.hourly[3].temp),\n       cell04: formatTemp(msg.payload.hourly[4].temp),\n       cell05: formatTemp(msg.payload.hourly[5].temp),\n       cell06: formatTemp(msg.payload.hourly[6].temp),\n       cell07: formatTemp(msg.payload.daily[1].temp.max, msg.payload.daily[0].temp.min),\n       cell08: formatTemp(msg.payload.daily[2].temp.max, msg.payload.daily[1].temp.min),\n       cell09: formatTemp(msg.payload.daily[3].temp.max, msg.payload.daily[2].temp.min),\n       cell10: formatTemp(msg.payload.daily[4].temp.max, msg.payload.daily[3].temp.min),\n  \t}\n  },\n  rowicons: {\n  \tdata01: {\n  \t\tcell01: msg.payload.hourly[1].weather[0].icon,\n  \t\tcell02: msg.payload.hourly[2].weather[0].icon,\n  \t\tcell03: msg.payload.hourly[3].weather[0].icon,\n  \t\tcell04: msg.payload.hourly[4].weather[0].icon,\n  \t\tcell05: msg.payload.hourly[5].weather[0].icon,\n  \t\tcell06: msg.payload.hourly[6].weather[0].icon,\n  \t\tcell07: msg.payload.daily[1].weather[0].icon,\n  \t\tcell08: msg.payload.daily[2].weather[0].icon,\n  \t\tcell09: msg.payload.daily[3].weather[0].icon,\n  \t\tcell10: msg.payload.daily[4].weather[0].icon,\n  \t}\n  }\n}\n\nreturn fcdata;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":760,"y":300,"wires":[["9dbf3c95.67c74"]]},{"id":"d8c5527f.7739","type":"ui_text","z":"5f633a28.607134","group":"70fb931.dfc146c","order":2,"width":2,"height":1,"name":"Temperature","label":"","format":"<p style=\"font-size: 200%\">{{msg.payload.current.temp}}</p>","layout":"row-left","x":990,"y":80,"wires":[]},{"id":"681b2b94.9f7a44","type":"http request","z":"5f633a28.607134","name":"Get OWM data","method":"GET","ret":"obj","paytoqs":true,"url":"https://api.openweathermap.org/data/2.5/onecall","tls":"","persist":false,"proxy":"","authType":"","x":520,"y":180,"wires":[["67075190.fc6f3","7adde26c.514aec"]]},{"id":"c72d96f0.204148","type":"inject","z":"5f633a28.607134","name":"Trigger","props":[{"p":"payload","v":"true","vt":"bool"},{"p":"topic","v":"","vt":"string"}],"repeat":"600","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"true","payloadType":"bool","x":120,"y":180,"wires":[["79276db2.a2ce14"]]},{"id":"67075190.fc6f3","type":"function","z":"5f633a28.607134","name":"Format current data","func":"var icon = {};\n\nvar units = flow.get('units');\nif (units === undefined) {units = 'imperial';}\n\nfunction timeConvert(UNIX_timestamp){\n  var dateObject = new Date(UNIX_timestamp * 1000);\n  if (flow.get('hour12')) {  // 12 hour time format\n    return dateObject.toLocaleString('en', { timezone: msg.payload.timezone, hour12: true, hour: 'numeric', minute: '2-digit'}).toLowerCase();\n  } else {  // 24 hour time format\n    return dateObject.toLocaleString('en', { timezone: msg.payload.timezone, hour12: false, hour: 'numeric', minute: '2-digit'});\n  }      \n}\n\nvar degreesToCardinal = function(deg){\n  if (deg>11.25 && deg<=33.75){return \"NNE\";}\n  else if (deg>33.75 && deg<56.25){return \"NE\";}\n  else if (deg>56.25 && deg<78.75){return \"ENE\";}\n  else if (deg>78.75 && deg<101.25){return \"E\";}\n  else if (deg>101.25 && deg<123.75){return \"ESE\";}\n  else if (deg>123.75 && deg<146.25){return \"SE\";}\n  else if (deg>146.25 && deg<168.75){return \"SSE\";}\n  else if (deg>168.75 && deg<191.25){return \"S\";}\n  else if (deg>191.25 && deg<213.75){return \"SSW\";}\n  else if (deg>213.75 && deg<236.25){return \"SW\";}\n  else if (deg>236.25 && deg<258.75){return \"WSW\";}\n  else if (deg>258.75 && deg<281.25){return \"W\";}\n  else if (deg>281.25 && deg<303.75){return \"WNW\";}\n  else if (deg>303.75 && deg<326.25){return \"NW\";}\n  else if (deg>326.25 && deg<348.75){return \"NNW\";}\n  else {return \"N\";}\n}\n\nif (units == \"imperial\")\n{\n  msg.payload.current.temp = msg.payload.current.temp.toFixed() + ' °F';\n  msg.payload.current.wind_speed = msg.payload.current.wind_speed.toFixed() + ' mph';\n}\nelse  // metric units\n{\n  msg.payload.current.temp = msg.payload.current.temp.toFixed(1) + ' °C';\n  msg.payload.current.wind_speed = msg.payload.current.wind_speed.toFixed(1) + ' m/s';\n}\n\nmsg.payload.current.wind_cardinal = degreesToCardinal(msg.payload.current.wind_deg);\n\nmsg.payload.current.sunrise = timeConvert(msg.payload.current.sunrise);\nmsg.payload.current.sunset = timeConvert(msg.payload.current.sunset);\n\nvar iconString = 'wi-owm-' + msg.payload.current.weather[0].icon + ' wi-4x';\nicon = {\n    ui_control: {\n        icon: iconString\n    }\n}; \n\nreturn [msg, icon];","outputs":2,"noerr":0,"initialize":"","finalize":"","x":750,"y":180,"wires":[["d8c5527f.7739","ec4eb2d0.974c","9c052db7.3c434","40f44662.3ecde8","5b97953.01fdd6c"],["180a101a.9cd43"]]},{"id":"79276db2.a2ce14","type":"change","z":"5f633a28.607134","name":"Settings","rules":[{"t":"delete","p":"payload","pt":"msg"},{"t":"set","p":"payload.lat","pt":"msg","to":"40.30","tot":"str"},{"t":"set","p":"payload.lon","pt":"msg","to":"-111.69","tot":"str"},{"t":"set","p":"payload.appid","pt":"msg","to":"<appID>","tot":"str"},{"t":"set","p":"payload.units","pt":"msg","to":"imperial","tot":"str"},{"t":"set","p":"payload.lang","pt":"msg","to":"en","tot":"str"},{"t":"set","p":"hour12","pt":"flow","to":"true","tot":"bool"},{"t":"set","p":"units","pt":"flow","to":"payload.units","tot":"msg"},{"t":"set","p":"lang","pt":"flow","to":"payload.lang","tot":"msg"},{"t":"set","p":"payload.exclude","pt":"msg","to":"minutely","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":320,"y":180,"wires":[["681b2b94.9f7a44"]]},{"id":"16181411.5a365c","type":"ui_ui_control","z":"5f633a28.607134","name":"Update tab","events":"all","x":90,"y":300,"wires":[["3f3d5ab9.82b3a6"]]},{"id":"c5efdf55.db32f","type":"link out","z":"5f633a28.607134","name":"Refresh","links":["a6b24cea.e52a7"],"x":1115,"y":240,"wires":[]},{"id":"a6b24cea.e52a7","type":"link in","z":"5f633a28.607134","name":"","links":["c5efdf55.db32f"],"x":155,"y":240,"wires":[["79276db2.a2ce14"]]},{"id":"3f3d5ab9.82b3a6","type":"switch","z":"5f633a28.607134","name":"tab focus","property":"tab","propertyType":"msg","rules":[{"t":"eq","v":"1","vt":"num"}],"checkall":"true","repair":false,"outputs":1,"x":260,"y":300,"wires":[["79276db2.a2ce14"]]},{"id":"70fb931.dfc146c","type":"ui_group","name":"MainGroup","tab":"4bb34f67.69a87","order":1,"disp":false,"width":10,"collapse":false},{"id":"4bb34f67.69a87","type":"ui_tab","name":"Weather","icon":"fa-thermometer-half","order":3,"disabled":false,"hidden":false}]

Flow Info

Created 5 years ago
Updated 4 years ago
Rating: 4.833333333333333 12

Owner

Actions

Rate:

Node Types

Core
  • change (x1)
  • comment (x1)
  • function (x2)
  • http request (x1)
  • inject (x1)
  • link in (x1)
  • link out (x1)
  • switch (x1)
Other

Tags

  • weather
  • dashboard
  • ui
  • openweathermap
Copy this flow JSON to your clipboard and then import into Node-RED using the Import From > Clipboard (Ctrl-I) menu option