Saturday, November 25, 2017

Node-RED: Calling REST Services and Building Our Own API

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

2 comments:

  1. 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?

    ReplyDelete
  2. Hi Vipul, just saw your message. What's up?

    ReplyDelete