Wednesday, December 28, 2016

An Alexa Skill for Integration with Weather Underground

When using Alexa with my Echo Dot, I found that the integrated weather forecast was not that accurate for my location.  Where I live, we have a significant microclimate, and getting the accurate forecast for my location is best done through Weather Underground.  I have my own personal weather station which provides a good forecast, and I wanted to link that in to my Alexa Flash briefing.  Unfortunately, there doesn't appear to be a Skill for that at this time, so I went through the process of creating my own and wanted to share details of that here.

The basic steps to set this up are as follows:
  • Get a Weather Underground API key to get the forecast and alert data in JSON format
  • Setup an Amazon Lambda function to read the WU API and translate it into a format that Alexa can understand for the Flash Briefing
  • Create an Amazon API interface to call the Lambda function
  • Setup the Alexa Skill for a Flash Briefing item which uses the API interface just setup
  • Turn on the skill on your Amazon Echo Dot (or other Alexa device)

Get a Weather Underground API key 

Sign for weatherunderground.com if you haven't already, and login to generate an API key.  The process is simple, and free for under 500 calls per month.  Just be sure and sign up for the "Cumulus" plan to get both forecast and alerts.  Once you have the API key, you'll be using two of the API calls to get the information for Alexa.  These are:

http://api.wunderground.com/api/xxxxxxxxxxxxxxx/forecast/q/pws:KVTSTARK3.json
http://api.wunderground.com/api/xxxxxxxxxxxxxxx/alerts/q/pws:KVTSTARK3.json

where xxxxxxxxxxxxxxx will be your WU API key.  

For the actual location, you will want to replace q/pws:KVTSTARK3.json with the refined location for you via weather underground.  To get this, go to the weather underground home page and look at the full forecast for your location.  If you have a personal weather station, you can just substitute KVTSTART3 with your PWS station id.  If not, then look at the URL of the full forecast, which will be something like this:

https://www.wunderground.com/q/zmw:05487.1.99999?sp=KVTBRIST11

and then replace the part of the url including and after the q (q/zmw:05487.1.99999?sp=KVTBRIST11 in the above example) with q/pws:KVTSTARK3 where KVTSTARK3 is your PWS station id.

Test out the urls in your browser to make sure you're getting back valid JSON, and then you're ready to move on to the next step.

Setup an Amazon Lambda function

Amazon Lambda functions provide quick and easy ways to implement snippets of code that are only executed when called.  They are efficient ways to handle API implementations without having a full-blown server running, and are only charged on the per-usage basis.

The assumption here is that you've already setup an AWS account.  From the AWS console, go to the Lambda service (https://console.aws.amazon.com/lambda/home).  You'll want to create a blank lambda function without any triggers at this time.  Call it something like getWUForecast and use the Node.js 4.3 runtime.    For the inline code, you can use the following code snippet:

'use strict';

console.log('Loading function');

exports.handler = (event, context, callback) => {

    var http = require('http');
    
    var alertsurl = "http://api.wunderground.com/api/xxxxxxxxxx/alerts/q/pws:KVTSTARK3.json"
    var forecasturl = "http://api.wunderground.com/api/xxxxxxxxxx/forecast/q/pws:KVTSTARK3.json"

    // Get alerts first
    http.get(alertsurl, function(res) {
        res.setEncoding('utf8');
        var rawData = "";
        res.on('data', (chunk) => {
            rawData += chunk;
        });
        res.on('end', () => {
            var alert = JSON.parse(rawData);

            // Now do the forecast portion
            var obj = doForecast(http, forecasturl, alert, function(obj) {
                callback(null, obj);
            });
            
        });
  
    }).on('error', function(e) {
        console.log("Got error: " + e.message);
        context.done(null, 'FAILURE');
    });      
};

// Handle forecast API call
function doForecast(http, url, alert, callback) {

    // Add in the alert to the beginning of the flash message     
    var alertObj = null;
    if (alert.alerts.length > 0) {
        console.log(alert.alerts[0].description);
        console.log(alert.alerts[0].message);
        alertObj = {
                "uid": "00000000-0000-1000-0000-000000000001",
                "updateDate":  new Date().toISOString(),
                "titleText": alert.alerts[0].description,
                "mainText": alert.alerts[0].message,
                "redirectionUrl": "https://www.weatherunderground.com"
        }
    }
            
    // Get the info from Weather Underground
    http.get(url, function(res) {
        res.setEncoding('utf8');
        var rawData = "";
        res.on('data', (chunk) => {
            rawData += chunk;
        });
        res.on('end', () => {
            var forecast = JSON.parse(rawData);
            
            // Put together all the next 4 forecast periods
            // TBD: Ideally we should check to make sure we have 4 items in the array
            var curForecast = "The current forecast for " + forecast.forecast.txt_forecast.forecastday[0].title;
            curForecast += " calls for ";
            curForecast += forecast.forecast.txt_forecast.forecastday[0].fcttext + " ";
            curForecast += "For " + forecast.forecast.txt_forecast.forecastday[1].title;
            curForecast += " " + forecast.forecast.txt_forecast.forecastday[1].fcttext + " ";
            curForecast += "For " + forecast.forecast.txt_forecast.forecastday[2].title;
            curForecast += " " + forecast.forecast.txt_forecast.forecastday[2].fcttext + " ";
            curForecast += "For " + forecast.forecast.txt_forecast.forecastday[3].title;
            curForecast += " " + forecast.forecast.txt_forecast.forecastday[3].fcttext + " ";
            
            // Setup the results for Alexa feed
            var forecastObj = {
                "uid": "00000000-0000-1000-0000-000000000002",
                "updateDate":  new Date().toISOString(),
                "titleText": forecast.forecast.txt_forecast.forecastday[0].title,
                "mainText": curForecast,
                "redirectionUrl": "https://www.weatherunderground.com"
               };

            var obj = null;
            if (alertObj !== null) {
                obj = [alertObj, forecastObj];
            } else {
                obj = [forecastObj];
            }
        
            callback(obj); 
        });
  
    }).on('error', function(e) {
        console.log("Got error: " + e.message);
    });
    console.log('end request to ' + url);   
}


After you've got the inline code, you also need to setup some config info.  Under Role, select to Create new role from template(s).  Give the role a name, such as WULambaRole and choose Simple Microservice Permissions as the template.  Everything else you can leave as defaults and then Next and Create Function.

You're now ready to integrate the lambda function to the API.

Create an Amazon API interface

Go to the Amazon console and navigate to the API Gateway services.  From there select Create API and give it a name such as WeatherUndergroundAPI.  Select Create API to create it.  Now select the root of the API (/) and under Action, select Create Method.  Select a GET method, a Lambda region where you created your Lambda function (probably us-east-1 if you didn't specify anything different before).  Enter your Lambda function name you created above (getWUForecast in the example) and save it.

Once the saving is complete, click on the GET method and then the TEST button to test it out.  If all is well you'll get a 200 status code and a JSON response that is formatted for Alexa.

Now you'll need to deploy the API.  Click on Actions, Deploy API.  Give it a new stage name of Production and then Deploy it.  Make not of the Invoke URL which will be something like this:

Invoke URL: https://xxxxxx.execute-api.us-east-1.amazonaws.com/Production

You'll need that URL to link to the Alexa Skill.

Setup the Alexa Skill

Now you'll need an Amazon Developer Account to create the Alexa skill in.  Go to the developer Alexa skills page and Add A New Skill.  You'll want to select a Skill Type of Flash Briefing Skill API and then give it a name, such as Weather Underground Skill.  Next through until you get to the Configuration tab and enter a custom error message.  Something like "The Weather Underground Skill is not currently available".

Now click on Add A New Feed  and a preamble to describe the feed, such as "Here is your weather briefing for Starksboro, VT".  Name the feed WeatherUnderground with content type of Text.  Select a genre of Weather and then enter the API Invoke URL you have from above in the URL field.  Click on Save and it will validate the link and continue on.

Next on the Test tab, flip the switch to ON so that you can integrate it with your Alexa.  Next on through and Save your skill.

Don't worry about publishing the skill, as this is just setting it up for your own personal use.  If you publish it, you run the risk of using up your Weather Underground API calls pretty quickly, as everyone will be using your API key then.

Turn on the skill

Finally, you need to turn the skill on in Alexa.  Go to the Alexa and navigate to Skills and then Your Skills.  The new skill will show up in the list, just click on it and then enable it.  After it's enabled, you can go to Manage in Flash Briefing to turn it on and set the order it shows up.  When it's ready, you can just go to Alexa and say "Give me my Flash Briefing" and it should all work.

That's it.  Hope this helps you in setting up a simple Alexa skill and doing some integration to Weather Underground!

4 comments:

  1. Hi, great !
    Tried and it works.
    Possible to get Data from my station from WU -> Alexa ?

    ReplyDelete
    Replies
    1. I would think so. Anywhere that you can do a query with the API and replace the q/pws:KVTSTARK3.json with your weather station ID should query it.

      Delete
  2. Hi!
    Great skill, i was watching for something like this.
    Maybe its possible to integrate some values from Conditions API
    http://api.wunderground.com/api/xxxxxxxxxxxxx/conditions/lang:DL/q/pws:xxxxxxxx.json

    Regards
    Martin

    ReplyDelete
    Replies
    1. Yes, that should be possible. I'll have to take a look at it and see what looks applicable.

      Delete