Getting Started with Cake Time: Using the Alexa Settings API to Look Up the Device Time Zone
Justin Jeffress Jul 02, 2019
Share:
Tutorial Beginner News
Blog_Header_Post_Img

Editor’s Note: We recently launched a new training course called Cake Time: Build an Engaging Alexa Skill. The course is a self-paced, skill-building course that offers guidance on how to build a high-quality skill from start to finish. In this 4-part blog series, we’re going to dive deep into the coding modules within the course.

Editor’s Note: We have changed the name of the Alexa skill in our beginner tutorial from Cake Walk to Cake Time given the term’s racially insensitive history.

 

In my previous post on adding memory to cake time, I walked through the process of adding memory to the Cake Time skill so your skill can remember your customer’s birthday. This fulfills a major portion of the skill design. Now all that’s left is to compute the number of days until their next birthday or wish them happy birthday if they are using the skill on their birthday.

Let's Review

Before we dive into implementation, let’s take a moment to review our voice-first design. There are three main scenarios:

  1. Birthday Unknown
  2. Birthday Known, Not Birthday
  3. Birthday Known, Birthday

Alexa Blog

We’ve fulfilled the first scenario since we can ask for and remember the customer’s birthday. Our design is flexible and, thanks to auto-delegation, we can ask follow-up questions if we don’t have enough information. We now have to finish the second and third scenarios. Both scenarios are dependent on the current date. If it’s their birthday, we need to say “happy birthday!” If it’s not, we need to count down the number of days until their next one. This means that our skill will need to figure out the current date.

Using the Right Date Is Important

How do we look up the date? Node.js provides a handy Dateobject that we can use to get the current date. Creating a new date without any parameters will get the current server time. Hold up! Did you notice anything suspicious about that last sentence? That’s right it’s the server time.

This is problematic. What if the server time is set to UTC (Coordinated Universal Time) and our customer is in Ohio? Ohio is in the eastern time zone and during summer is 4 hours behind UTC. If our customer were to use the skill on the eve of their birthday any time after 7:59pm, the skill would wish them “happy birthday.” That’s too early! On their birthday, if they opened the skill after 7:59pm it would tell them that there are 364 days left until their next one. That’s even worse because we missed wishing them a happy birthday on their birthday!

If they were located in Tokyo, Japan they would be 9 hours ahead of UTC. In this case, we’d be 9 hours too slow! The only case where our skill would be accurate is if our customer lived in the same timezone as our skill’s Lambda function. We can’t rely on that! We need a way to get the timezone where our customer is located.

Understanding the Alexa Settings API

To do that, we can use the Alexa Settings API. The API will return the time zone that our customer’s device is set to. We can use that to make our date calculations totally accurate and avoid the embarrassing situation where we wish them a happy birthday too soon or too late. The API is device specific. Our customer can have multiple devices and each device has a separate time zone setting. Let’s say our customer is traveling on their birthday from Ohio to Arizona and they check the skill using the Alexa app from Arizona on their phone. The time zone will have automatically updated during their travels and will be different than the Alexa-enabled device they left at home on their desk. To make use of the Alexa Settings API, we’ll need to pass the device’s ID via a web request. The web request will return the time zone as a string to our skill. You can also use the Alexa Settings API to look up the distance and temperature measurement units.

Using the Alexa Settings API to Get the Time Zone

Now that we understand that we can get the time zone using the Alexa Settings API, let’s take a look at how we used it from our skill. We’ll need to make a separate web request to look up the time zone. We could use the http module to build and make the request, but the software development kit (SDK) includes an API client that is preconfigured to work with the Alexa Settings API. We’ll simply need to create a new DefaultAPIClient and register it with our SkillBuilder using the withApiClient function:

Copied to clipboard
exports.handler = Alexa.SkillBuilders.custom()
    .withPersistenceAdapter(
        new persistenceAdapter.S3PersistenceAdapter({bucketName:process.env.S3_PERSISTENCE_BUCKET})
    )
    .addRequestHandlers(
        HasBirthdayLaunchRequestHandler,
        LaunchRequestHandler,
        CaptureBirthdayIntentHandler,
        HelpIntentHandler,
        CancelAndStopIntentHandler,
        SessionEndedRequestHandler,
        IntentReflectorHandler) // make sure IntentReflectorHandler is last so it doesn't override your custom intent handlers
    .addErrorHandlers(
        ErrorHandler
    )
    .addRequestInterceptors(
        LoadBirthdayInterceptor
    )
    .withApiClient(new Alexa.DefaultApiClient())
    .lambda(); 

Now that we’ve registered the client, we can now make use of it. To do that, we’ll need to use the UpsServiceClientto talk to the Alexa Settings API. The SDK provides a factory method that creates the client for us. Since we registered the DefaultApiClientthe factory method will link it to the UpsServiceClient. Let’s see what that looks like in code:

const serviceClientFactory = handlerInput.serviceClientFactory;
const upsServiceClient = serviceClientFactory.getUpsServiceClient();

The serviceClientFactoryis part of the handlerInputobject so for convenience we store it in a variable. The on the second line we call the getUpsServiceClientfunction and save it into the upServiceClientvariable.

For the next step, we the need device ID. The device ID is included in every request that is sent to our skill. We can get the device ID from the deviceIdfield. Below is a snippet of the request that is sent to our skill.

Copied to clipboard
{
  "context": {
    "System": {
      "apiAccessToken": "AxThk...",
      "apiEndpoint": "https://api.amazonalexa.com",
      "device": {
        "deviceId": "string-identifying-the-device",
        "supportedInterfaces": {}
      },
      "application": {
        "applicationId": "string"
      },
      "user": {}
    }
  }
  ...
}    

Let’s take a look at how we traverse the JSON to get the deviceID:

context.System.device.deviceId

The request is included in the requestEnvelopewhich is part of the handlerInputobject. So putting it all together looks like:

const deviceId = handlerInput.requestEnvelope.context.System.device.deviceId;

With all the set up out of the way, we can now get the time zone! To do that we can use the getSystemTimeZonefunction and pass it deviceId:

userTimeZone = await upsServiceClient.getSystemTimeZone(deviceId);

Since the function is making a web service call for us is asynchronous and returns a promise we can use the async/awaitkeywords so our code will pause until getSystemTimeZonefinishes looking up the timezone. Since we’re making a webservice call there is a chance that an error could occur like a timeout. We should wrap our call in a try/catchblock so if theres an error we can gracefully recover. Let’s do that and zoom out so you can see it all in one glance:

Copied to clipboard
async handle(handlerInput) {
    const serviceClientFactory = handlerInput.serviceClientFactory;
    const deviceId = handlerInput.requestEnvelope.context.System.device.deviceId;
      
    let userTimeZone;
    try {
        const upsServiceClient = serviceClientFactory.getUpsServiceClient();
        userTimeZone = await upsServiceClient.getSystemTimeZone(deviceId);    
    } catch (error) {
        if (error.name !== 'ServiceError') {
            return handlerInput.responseBuilder.speak("There was a problem connecting to the service.").getResponse();
        }
        console.log('error', error.message);
    }
    console.log('userTimeZone', userTimeZone);
}

Now that we have the timezone we can do some math to figure out how many days remain until their next birthday. The solution we came up with isn’t the end-all, be-all solution. If you find a better way please do so. For our solution we’ll be using the javascript date object and the timezone of the device. First we’ll get the current time based on userTimeZone using the toLocaleString function. When we create a date, it appends the current hour, minute and second to the date. We didn’t ask our customer the exact time they were born nor do we want our skill to only wish a happy birthday at that exact time. We can strip off the time by creating a new date with just the year, month, and day.

Copied to clipboard
// getting the current date with the time
const currentDateTime = new Date(new Date().toLocaleString("en-US", {timeZone: userTimeZone}));
// removing the time from the date because it affects our difference calculation
const currentDate = new Date(currentDateTime.getFullYear(), currentDateTime.getMonth(), currentDateTime.getDate());
const currentYear = currentDate.getFullYear();

Now that we have the currentDate without the time, we’ll use our customer’s birthday month and day and combine it with the year of currentDate to define nextBirthday. We’ll compare that with the currentDate. If the dates are equal, then we can wish our customer happy birthday. If nextBirthday is larger than nextBirthday then we need to count down. If nextBirthday is less than the currentDate then their birthday has passed this year and so we need to increment the the year by one and then countdown. So while we have three conditions we only have two outcomes, 1. wish happy birthday 2. countdown. To save ourselves some logic, we’ll just check if currentDate is larger than nextBirthday and if so increment the year.

Copied to clipboard
// getting getting the next birthday
let nextBirthday = Date.parse(`${month} ${day}, ${currentYear}`);
    
// adjust the nextBirthday by one year if the current date is after their birthday
if (currentDate.getTime() > nextBirthday) {
    nextBirthday = Date.parse(`${month} ${day}, ${currentYear + 1}`);
}

Now that we’ve adjusted nextBirthday, we’re almost done. We now need to craft our message based on if it’s their birthday or not. We could do it with an if-else check, but why make our logic so complicated? It doesn’t take much to set our default message to “Happy Birthday” and then override that message with the countdown if it’s not their birthday.

Copied to clipboard
// setting the default speechText to Happy xth Birthday!! 
// Alexa will automatically correct the ordinal for you.
// no need to worry about when to use st, th, rd
let speechText = `Happy ${currentYear - year}th birthday!`;

To check if it’s not their birthday, all we need to do is check if currentDate is not equal to nextBirthday and then calculate the remaining days left until their next birthday. We’ll do that by first computing how many milliseconds are in a day so we can convert the difference between the two dates in milliseconds back to days.

Copied to clipboard
const oneDay = 24*60*60*1000;
if (currentDate.getTime() !== nextBirthday) {
    const diffDays = Math.round(Math.abs((currentDate.getTime() - nextBirthday)/oneDay));
    speechText = `Welcome back. It looks like there are ${diffDays} days until your ${currentYear - year}th birthday.`
}

Now that we’ve figured out what our skill needs to say, all we need to do now is return a response.

Copied to clipboard
return handlerInput.responseBuilder
    .speak(speechText)
    .getResponse();

Now that we understand what’s going on let’s zoom out to take a look at the whole thing:

Copied to clipboard
async handle(handlerInput) {
    
    const serviceClientFactory = handlerInput.serviceClientFactory;
    const deviceId = handlerInput.requestEnvelope.context.System.device.deviceId;
    
    const attributesManager = handlerInput.attributesManager;
    const sessionAttributes = attributesManager.getSessionAttributes() || {};
    
    const year = sessionAttributes.hasOwnProperty('year') ? sessionAttributes.year : 0;
    const month = sessionAttributes.hasOwnProperty('month') ? sessionAttributes.month : 0;
    const day = sessionAttributes.hasOwnProperty('day') ? sessionAttributes.day : 0;
    
    let userTimeZone;
    try {
        const upsServiceClient = serviceClientFactory.getUpsServiceClient();
        userTimeZone = await upsServiceClient.getSystemTimeZone(deviceId);    
    } catch (error) {
        if (error.name !== 'ServiceError') {
            return handlerInput.responseBuilder.speak("There was a problem connecting to the service.").getResponse();
        }
        console.log('error', error.message);
    }
    console.log('userTimeZone', userTimeZone);
            
    // getting the current date with the time
    const currentDateTime = new Date(new Date().toLocaleString("en-US", {timeZone: userTimeZone}));
    // removing the time from the date because it affects our difference calculation
    const currentDate = new Date(currentDateTime.getFullYear(), currentDateTime.getMonth(), currentDateTime.getDate());
    const currentYear = currentDate.getFullYear();
    
    console.log('currentDateTime:', currentDateTime);
    console.log('currentDate:', currentDate);
    
    // getting getting the next birthday
    let nextBirthday = Date.parse(`${month} ${day}, ${currentYear}`);
    
    // adjust the nextBirthday by one year if the current date is after their birthday
    if (currentDate.getTime() > nextBirthday) {
        nextBirthday = Date.parse(`${month} ${day}, ${currentYear + 1}`);
    }
    
    // setting the default speechText to Happy xth Birthday!! 
    // Alexa will automatically correct the ordinal for you.
    // no need to worry about when to use st, th, rd
    let speechText = `Happy ${currentYear - year}th birthday!`;
    
    const oneDay = 24*60*60*1000;
    if (currentDate.getTime() !== nextBirthday) {
        const diffDays = Math.round(Math.abs((currentDate.getTime() - nextBirthday)/oneDay));
        speechText = `Welcome back. It looks like there are ${diffDays} days until your ${currentYear - year}th birthday.`
    }
    
    return handlerInput.responseBuilder
        .speak(speechText)
        .getResponse();
}

Conclusion

At this point, we’ve implemented the core set of features we set out to build. We have a skill that reacts to the customer based upon the current situation. If it doesn’t know the customer’s birthday, it can ask for it. If the customer didn’t provide all the necessary information, much like a human conversational partner, it can respond with follow-up questions. Once the skill knows their birthday, it can wish our customer a happy birthday on their special day and count down the days until their next one when it’s not. The core of the experience is complete, but we shouldn’t stop here. We can continue to build upon our skill and enhance the experience. We could use Alexa Presentation Language to show a birthday card and a countdown clock on an Alexa-enabled device with a screen. We could offer some in-skill products like a subscription for a daily horoscope. We could also surprise and delight our customers by telling them that they share a birthday with a famous celebrity.

Let’s keep the conversation going. If you have any questions or comments about memory and cake time reach out to me on Twitter @SleepyDeveloper.

Related Content