The goal for this tutorial is to build a Node-RED flow that grabs weather JSON from Weather Underground and presents the results in a web page. Our Node-RED service will accept requests of the form
/weather?zip=<<zipcode>>It will call the Weather Underground API for that zipcode, then return the data formatted as HTML. Only simple error handling will be implemented.
The Weather Underground API
Weather Underground provides an API that provides current weather conditions based on zipcode, etc. To use this API, it will be necessary to create a free developer account, called the "Stratus Plan", which allows for 500 API calls per day and at most 10 calls per minute.
Use that account to generate an API key. Using that key along with their conditions API, weather data for zipcode 18901 can be retrieved as follows:
http://api.wunderground.com/api/<<API_key_goes_here>>/features/conditions/q/18901.json
Here's an example response. The returned JSON is overkill, and there doesn't seem to be a way to specify which fields to return. The fields we'll be using are highlighted in yellow:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | { "response": { "version": "0.1", "termsofService": "http://www.wunderground.com/weather/api/d/terms.html", "features": { "conditions": 1 }, "error": { "type": "unknownfeature" } }, "current_observation": { "image": { "url": "http://icons.wxug.com/graphics/wu2/logo_130x80.png", "title": "Weather Underground", "link": "http://www.wunderground.com" }, "display_location": { "full": "Doylestown, PA", "city": "Doylestown", "state": "PA", "state_name": "Pennsylvania", "country": "US", "country_iso3166": "US", "zip": "18901", "magic": "1", "wmo": "99999", "latitude": "40.31000137", "longitude": "-75.12999725", "elevation": "128.6" }, "observation_location": { "full": "Doylestown Harvey, Doylestown, Pennsylvania", "city": "Doylestown Harvey, Doylestown", "state": "Pennsylvania", "country": "US", "country_iso3166": "US", "latitude": "40.312313", "longitude": "-75.135078", "elevation": "289 ft" }, "estimated": {}, "station_id": "KPADOYLE20", "observation_time": "Last Updated on November 23, 8:12 PM EST", "observation_time_rfc822": "Thu, 23 Nov 2017 20:12:54 -0500", "observation_epoch": "1511485974", "local_time_rfc822": "Thu, 23 Nov 2017 20:12:56 -0500", "local_epoch": "1511485976", "local_tz_short": "EST", "local_tz_long": "America/New_York", "local_tz_offset": "-0500", "weather": "Clear", "temperature_string": "29.3 F (-1.5 C)", "temp_f":29.3, "temp_c":-1.5, "relative_humidity": "62%", "wind_string": "Calm", "wind_dir": "South", "wind_degrees":182, "wind_mph":0.0, "wind_gust_mph":0, "wind_kph":0, "wind_gust_kph":0, "pressure_mb": "1017", "pressure_in": "30.04", "pressure_trend": "-", "dewpoint_string": "18 F (-8 C)", "dewpoint_f":18, "dewpoint_c":-8, "heat_index_string": "NA", "heat_index_f": "NA", "heat_index_c": "NA", "windchill_string": "29 F (-2 C)", "windchill_f": "29", "windchill_c": "-2", "feelslike_string": "29 F (-2 C)", "feelslike_f": "29", "feelslike_c": "-2", "visibility_mi": "10.0", "visibility_km": "16.1", "solarradiation": "1", "UV": "0.0","precip_1hr_string": "0.00 in ( 0 mm)", "precip_1hr_in": "0.00", "precip_1hr_metric": " 0", "precip_today_string": "0.00 in (0 mm)", "precip_today_in": "0.00", "precip_today_metric": "0", "icon": "clear", "icon_url": "http://icons.wxug.com/i/c/k/nt_clear.gif", "forecast_url": "http://www.wunderground.com/US/PA/Doylestown.html", "history_url": "http://www.wunderground.com/weatherstation/WXDailyHistory.asp?ID=KPADOYLE20", "ob_url": "http://www.wunderground.com/cgi-bin/findweather/getForecast?query=40.312313,-75.135078", "nowcast": "" } } |
Our Node-RED flow should be able to handle some error conditions. Here's one example of an error response from the conditions API:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | { "response": { "version": "0.1", "termsofService": "http://www.wunderground.com/weather/api/d/terms.html", "features": { "conditions": 1 }, "error": { "type": "querynotfound", "description": "No cities match your search query" } } } |
It was generated by requesting weather for a non-existent zipcode, like this:
http://api.wunderground.com/api/<<API_key_goes_here>>/features/conditions/q/99999.json
The Flow
Now, onto our goal of writing a Node-RED flow to call that API and format the response as pretty HTML!
For the flow, we will be using the following components:
- HTTP In
- Listen for requests of the form /weather?zip=<<zipcode>>
- Function
- Extract the zipcode out of the request query params, and build the Weather Underground API call
- HTTP Request
- Make the API call and when we get a response...
- Switch
- ... check to see if it was an error
- Template
- Extract data from the response JSON and format it into an HTML page
- HTTP Response
- Return the HTML generated by the template node
- Debug
- Show API URL as well as the Weather Underground response
The nodes are connected with wires as follows:
Note that the two Debug nodes near the top are redundant, since both are outputting the msg object! If we wanted to, we could wire the outputs from the HTTP In node and the "Set URL" Function node into one Debug node.
The code for the Set URL Function node is as follows:
1 2 | msg.url = "http://api.wunderground.com/api/<<API_key_goes_here>>/features/conditions/q/" + msg.req.query.zip + ".json"; return msg; |
In the HTTP Request node, set the return type to be "a parsed JSON object".
In the "Check for Error" Switch node, if msg.payload.response.error.type equals "unknownfeature" we route to the "success" template, otherwise we route to the error template. Here's what the Switch node configuration form looks like:
The template for when Weather Underground successfully returned data is as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <!DOCTYPE html> <html> <head> <title>Weather!</title> <style>body {font-family: sans-serif}</style> </head> <body> <h1>Weather in {{payload.current_observation.display_location.full}}</h1> <h2>{{payload.current_observation.weather}} <img src="{{payload.current_observation.icon_url}}" /></h2> Temperature: {{payload.current_observation.temperature_string}}<br /> Humidity: {{payload.current_observation.relative_humidity}}<br /> Pressure: {{payload.current_observation.pressure_in}} inHg<br /> Wind: {{payload.current_observation.wind_string}}<br /> <h6>{{payload.current_observation.observation_time}}</h6> <a href="{{{payload.current_observation.image.link}}}" target="_blank"><img src="{{{payload.current_observation.image.url}}}" /></a> </body> </html> |
Mustache template tags are used in the HTML body. Notice that "triple mustaches" are used on line 18 since we don't want Mustache to escape the URL.
When an error condition is detected, the flow will use this template to report an error:
1 2 3 4 5 6 7 8 9 10 | <html> <head> <title>Weather!</title> <style>body {font-family: sans-serif}</style> </head> <body> <h1>Error Getting Weather</h1> <h2>{{payload.response.error.description}}</h2> </body> </html> |
Open a browser and go to http://127.0.0.1:1880/weather?zip=18901, and something like the following page will be presented:
But if you go to http://127.0.0.1:1880/weather?zip=99999, you'll get the error page:
Finally, here is the complete source code to that flow, minus my API key, of course!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | [ { "id": "4ece1ecb.8096b", "type": "http in", "z": "7651fb0.bb59804", "name": "", "url": "/weather", "method": "get", "upload": false, "swaggerDoc": "", "x": 110, "y": 120, "wires": [ [ "658ac2c5.bc83cc", "ca471dea.0d8d" ] ] }, { "id": "869b03f7.a180c", "type": "http request", "z": "7651fb0.bb59804", "name": "Get Weather", "method": "GET", "ret": "obj", "url": "", "tls": "", "x": 270, "y": 280, "wires": [ [ "b763dd13.c9185", "b54ab768.1f6298" ] ] }, { "id": "7b4c5ed6.4422d", "type": "template", "z": "7651fb0.bb59804", "name": "Show Weather", "field": "payload", "fieldType": "msg", "format": "handlebars", "syntax": "mustache", "template": "<!DOCTYPE html>\n<html>\n <head>\n <title>Weather!</title>\n <style>body {font-family: sans-serif}</style>\n </head>\n <body>\n <h1>Weather in {{payload.current_observation.display_location.full}}</h1>\n <h2>{{payload.current_observation.weather}} <img src=\"{{payload.current_observation.icon_url}}\" /></h2>\n\n Temperature: {{payload.current_observation.temperature_string}}<br />\n Humidity: {{payload.current_observation.relative_humidity}}<br />\n Pressure: {{payload.current_observation.pressure_in}} inHg<br />\n Wind: {{payload.current_observation.wind_string}}<br />\n\n <h6>{{payload.current_observation.observation_time}}</h6>\n\n <a href=\"{{{payload.current_observation.image.link}}}\" target=\"_blank\"><img src=\"{{{payload.current_observation.image.url}}}\" /></a>\n </body>\n</html>", "output": "str", "x": 700, "y": 240, "wires": [ [ "7a405c32.7c1134" ] ] }, { "id": "7a405c32.7c1134", "type": "http response", "z": "7651fb0.bb59804", "name": "", "statusCode": "", "headers": {}, "x": 890, "y": 280, "wires": [] }, { "id": "7b473466.a14c6c", "type": "debug", "z": "7651fb0.bb59804", "name": "", "active": true, "console": "false", "complete": "true", "x": 450, "y": 120, "wires": [] }, { "id": "658ac2c5.bc83cc", "type": "debug", "z": "7651fb0.bb59804", "name": "", "active": true, "console": "false", "complete": "true", "x": 310, "y": 40, "wires": [] }, { "id": "ca471dea.0d8d", "type": "function", "z": "7651fb0.bb59804", "name": "Set URL", "func": "msg.url = \"http://api.wunderground.com/api/<<API_key_goes_here>>/features/conditions/q/\" + msg.req.query.zip + \".json\";\nreturn msg;", "outputs": 1, "noerr": 0, "x": 280, "y": 120, "wires": [ [ "869b03f7.a180c", "7b473466.a14c6c" ] ] }, { "id": "b763dd13.c9185", "type": "debug", "z": "7651fb0.bb59804", "name": "", "active": true, "console": "false", "complete": "false", "x": 470, "y": 400, "wires": [] }, { "id": "b54ab768.1f6298", "type": "switch", "z": "7651fb0.bb59804", "name": "Check for Error", "property": "payload.response.error.type", "propertyType": "msg", "rules": [ { "t": "eq", "v": "unknownfeature", "vt": "str" }, { "t": "neq", "v": "unknownfeature", "vt": "str" } ], "checkall": "true", "outputs": 2, "x": 480, "y": 280, "wires": [ [ "7b4c5ed6.4422d" ], [ "db257584.8aa478" ] ] }, { "id": "db257584.8aa478", "type": "template", "z": "7651fb0.bb59804", "name": "Show Error", "field": "payload", "fieldType": "msg", "format": "handlebars", "syntax": "mustache", "template": "<html>\n <head>\n <title>Weather!</title>\n <style>body {font-family: sans-serif}</style>\n </head>\n <body>\n <h1>Error Getting Weather</h1>\n <h2>{{payload.response.error.description}}</h2>\n </body>\n</html>", "output": "str", "x": 690, "y": 320, "wires": [ [ "7a405c32.7c1134" ] ] } ] |
Hello,I am Vipul and i am currently trying to understand and work with API in node-red and i am stuck with a problem.By looking at your post i assumed that you have good knowledge of API in node-red and i was wondering if you could help me solve my problem with API's in node-red?
ReplyDeleteHi Vipul, just saw your message. What's up?
ReplyDelete