Alexa, ask UA Campus Rec when does the gym open?

I built an Alexa Skill, and got it published.

How I Learned Alexa Skill Kit and Why I Picked it Up Again

I learned about Alexa Skill Kit at Hack Arizona 2017, and built an incredibly complicated 520bus skill to query departure times of Sun Tran buses and Sun Link modern streetcar. I didn't win a prize, and quickly forgot about that.

A few months later, a tweet caught my attention: publish a skill, get an Echo Dot & Alexa dev swag. I always wanted to own a hardware device that integrates with Alexa. I considered the "practically free" Dash Wand, but gave up when I found out it is powered by batteries. I couldn't afford to buy an Echo, because I already spent all my money on those ESP8266 toys. However, I'm not going to miss a FREE Echo Dot.

Resurrecting 520bus would not be an easy task, because 520bus is implemented on the IBM Cloud, and my trial has run out. I decide to make a very simple, but still useful skill, so that I can snag the Echo Dot without spending too much time. I turned my eye to the beloved Campus Recreation Center, in which I spent a significant part of my life during the past 3 years. How about a skill to query Rec hours?

Scraping Data Source

I started building the skill "UA Campus Rec". I planned two features: check the building hours, and check the pool hours. The information is right on the Rec homepage:

Campus Rec homepage hours display

This isn't my first time scraping the Campus Rec website.

I choose to build my skill on the Losant IoT platform. Building an Alexa Skill on Losant requires (almost) no code, and they added a HTML Parse node following my suggestion so it is easier to scrape web pages.

Scraping the Rec website for building and pool hours needs three nodes in a Losant workflow:

Losant workflow to scrape a webpage

  • An HTTP node downloads the http://rec.arizona.edu/ web page.
  • Two HTML parse nodes extract the relevant information from the web page.

HTML parser node configuration to extract pool hours

A quick test (with a virtual button) confirms this works well:

debug of scraping

Voice Interaction Model

For my two features, the primary questions a user may ask my skill are:

  • Alexa, ask u.a. campus rec when does the building open?
  • Alexa, ask u.a. campus rec when does the pool open?

I also need to accept a few variants to "building", such as "facility", "rec", "gym", "weight room", etc. Since everything except the pool always opens and closes at the same time, these words mean the same as "building".

A single intent covers both questions:

{
  "intents": [
    {
      "slots": [
        {
          "name": "Facility",
          "type": "FACILITY_WITH_HOURS"
        }
      ],
      "intent": "HoursIntent"
    }
  ]
}

In this intent, FACILITY_WITH_HOURS is a custom slot type that accepts all variants of "building", plus the "pool".

Then I thought about a few other question forms:

  • When does the gym close?
  • Ask u.a. campus rec about building hours.
  • Ask u.a. campus rec for open time.

All question forms are covered by my sample utterances:

HoursIntent open time
HoursIntent hours of {Facility}
HoursIntent {Facility} hours
HoursIntent when does the {Facility} open
HoursIntent when does the {Facility} close

These constitute my initial voice interaction model.

Losant Workflow for Alexa Skill

Following Losant's Alexa Skill guide, I created a webhook for Alexa to invoke, and receive requests into a workflow.

Losant workflow chart

The workflow contains the following parts:

  1. First, there are some sanity checks, such as APPID must match the expectation.
  2. Alexa Skill Kit has three request types: IntentRequest, LaunchRequest, and SessionEndedRequests. Each must be handled differently.
  3. Within IntentRequest, each intent should be responded differently.

HoursIntent is the only path where scraping is needed. After downloading the web page and extracting the hours, a little bit of JavaScript determines whether the user is asking about the pool or the rest of Campus Rec, and chooses a response accordingly:

var hours;
if (payload.req.request.intent.slots.Facility.value == 'pool') {
  payload.location = 'the pool';
  hours = payload.poolHours;
}
else {
  payload.location = 'campus rec';
  hours = payload.bldgHours;
}
payload.hours = {
  open: hours[0],
  close: hours[1]
};

The voice response is then produced with a Webhook Reply node:

{
  "version": "1.0",
  "response": {
    "outputSpeech": {
      "type": "PlainText",
      "text": "{{location}} is open {{hours.open}} to {{hours.close}} today."
    }
  }
}

Download: Alexa UA Campus Rec workflow

Alexa Skill Submission

Before I could submit the skill for certification, I need to complete a few description text boxes, and provide a set of logos. Since unofficial apps are not permitted to use University of Arizona's "block A" logo, I searched for a loyalty-free dumbbell icon to represent "the gym", as the logo of my unofficial UA Campus Rec skill. Friday night, just two hours after I started building the skill, it is sent off for certification.

Monday morning, I received an email:

Your skill submission has failed the certification process. We’ve included a description of the issue(s) and steps to reproduce below. Please address these and resubmit the skill at your earliest convenience. As a reminder, when you publish a skill between July 1, 2017 and July 31, 2017, you can apply for a free Echo Dot to commemorate your achievement. Quantities are limited. See our terms and conditions. Non-US developers, check out our other promotions in the UK, Germany, and India.

Issues with skill endpoint validation

The skill does not validate the signature for the incoming requests and is accepting requests with invalid signature specified. Please refer to the document that describes how to build your Alexa Skill as a web service for tips and requirements about validating the requests (and their signatures) sent to your web service.

Issues with skill in English (US)

When users ask for "help" within the skill, it must return a prompt which instructs users how to navigate the skill’s core functionality. Additionally, the help prompt must end with a question for users and leave the session open to receive a response.

Steps to reproduce: User: "Alexa, open u. a. campus rec" Skill: "This is the unofficial skill for University of Arizona Campus Recreation Center. You may ask me about building hours, pool hours." User: "help" Skill: "campus rec is open 10:00am to 10:00pm today." And session goes off.

Please see test case 4.12 from our Submission Checklist for guidance on the help intent.

The certification team has been hard at work during the weekend, and the feedback is very detailed. They also didn't forget to remind about the Echo Dot, which is exactly why I'm building the skill.

Why My Skill isn't "help"ing?

As you can see from the Losant workflow, I did implement the HelpIntent. Why is the skill speaking the building hours when it's asked for "help"?

The reason is in the docs:

To implement a built-in intent, you need to add the intent to your intent schema, then add handling for the intent to your code.

Although the workflow handles AMAZON.HelpIntent, I forgot to add it to the voice interaction model. Add them, and it works like a charm:

{
  "intents": [
    {
      "slots": [
        {
          "name": "Facility",
          "type": "FACILITY_WITH_HOURS"
        }
      ],
      "intent": "HoursIntent"
    },
    {
      "intent": "AMAZON.HelpIntent"
    },
    {
      "intent": "AMAZON.StopIntent"
    },
    {
      "intent": "AMAZON.CancelIntent"
    }
  ]
}

Skill Endpoint Validation

Apparently, I'm not the first one running into this problem on Losant. The short answer from Losant is that you have to stick with Amazon Lambda when publishing a skill. However, I want to save my 1-year of free AWS EC2 trial credit for some years later, so I don't want to enable Lambda right now. I need a different solution.

I ended up using a PHP script on my server to do the signature verification:

<?php
// composer require froodley/amazon-alexa-php:0.5.2.4
require_once $_SERVER['DOCUMENT_ROOT'].'/vendor/autoload.php';

use Alexa\Utility\Purifier\PurifierFactory;

class AlexaCertificate extends \Alexa\Request\Certificate
{
  protected function validateTimestamp($timestamp)
  {
    // Generate DateTimes
    $currentDateTime = new \DateTime();
    $timestamp = new \DateTime((is_int($timestamp) ? '@' : '').$timestamp);

    // Compare
    $differenceInSeconds = $currentDateTime->getTimestamp() - $timestamp->getTimestamp();
    if ($differenceInSeconds > self::TIMESTAMP_VALID_TOLERANCE_SECONDS) {
        throw new \InvalidArgumentException(self::ERROR_REQUEST_EXPIRED);
    }
  }
}

$rawJson = file_get_contents('php://input');
$cert = new AlexaCertificate($_SERVER['HTTP_SIGNATURECERTCHAINURL'], $_SERVER['HTTP_SIGNATURE'], PurifierFactory::generatePurifier(PurifierFactory::DEFAULT_CACHE_PATH));
$cert->validateRequest($rawJson);

header('Content-type: application/json');
$ch = curl_init('https://triggers.losant.com/webhooks/<webhook-id>');
curl_setopt($ch, CURLOPT_HEADER, FALSE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, FALSE);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-type: application/json'));
curl_setopt($ch, CURLOPT_POST, TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS, $rawJson);
curl_exec($ch);
?>

I had to override a function in \Alexa\Request\Certificate class, because Alexa is sending me the timestamp as an integer instead of a string, but the library cannot handle that correctly.

Another problem I have to work around is: Amazon is part of an anti-Let's Encrypt movement. According to certification requirements, Amazon accepts certificates issued by any CA in Mozilla's list, with the exception of letsencrypt.org. The solution is to proxy the traffic through Cloudflare, so that Amazon sees a certificate issued by COMODO.

Skill is Live

With both modifications in place, I re-submitted the skill for certification. Wednesday morning, Alexa UA Campus Rec skill is published! Now I'll just wait for my free Echo Dot.