Monday, April 6, 2020

Creating a prayer times action on Google Home

Saving a lot of time during the COVID-19 shutdown gave me the chance to do something I wanted to try for a while, getting my Google Home Mini to tell me the prayer time instead of looking at a screen. So this weekend I got to play with a dummy piece of code I made during the week and finalize my test version of my own prayer times assistant.

So far, I can only ask about the next prayer or name a specific prayer to know its time.

Important note: you cannot replicate the following project without a premium account on Firebase to enable calls to external APIs.



Here is what I had to do in simple words:

1- Create a new Dialogflow agent


Dialogflow is where I started my project with creating a blank agent with nothing but a Welcome intent.



2 - Modify the default welcome intent


For the sake of having fun, I changed the way of addressing the user to say the magic word: "Salam! How can I help you?" plus accepting similar magic words like "Assalm Alaykom".



3 - Create a new intent for the next prayer time


This intent is meant to looks for the next prayer time in the array of today's prayers. But first, in which city or location?

Circumvent the location access step


To get the user's location, I need to ask for permissions. I didn't want one extra step to ask for permission. I actually don't know if I will have to do this every time or I can add checks once and then I have the location for always. But to cut the working time, I decided to let the user simply mention the city. This can make it faster to develop my agent for now. So I added some training phrases with and without the city parameter. But notice that I added the city parameter as required and added some prompts to ask the user if the parameter is not found in the phrase. I also helped the training by double-clicking the word "Berlin" and chose my city parameter from that popup list which appeared.



Use fulfillment for fetching prayer times


To get to write some code to get the answer I need, I should enable fulfillments (enable webhooks checkbox). This is a special section where you can programmatically manipulate your agent. I also marked this intent as an end of conversation. This means that once I respond, the agent stops and does not listen to any more input from the user.



I will get to the fulfillments section a bit later.

4 - Create a new intent for a specific prayer time


If I already can fetch and scan through some prayer times, creating the next intent was pretty easy in comparison to the previous one. That's because I includes no comparisons or calculations, but as simple as looking for a specific key in the prayers key-value object.

Create a Prayer entity


To detect the prayer in the user's phrase, I had to create a special entity which does not exist by default, which is the prayer entity.

This entity can be created from the left side panel. In the entity, I define the possible entries, along with any synonyms which can mean the same thing. So I added the names of the five prayers and added some synonyms for any different pronunciation.



Create the intent with the phrases and responses like before


Just like the previous intent, I created a new one and added some training phrases and responses. And this time I added one more parameter: prayer. and I set its entity as Prayer, which I just created. And then I marked the prayer in each training phrase by marking the text and choosing the correct parameter from the list appearing.




Enable fulfillment and end of conversation


This step is similar to the previous entity. I enabled fulfillments and marked the intent as end of conversation.




5 - Fulfillment section


Now comes the programmatic part. In this section, we should enable the inline editor. This is where we write handlers to do what we want. There is another option to have webhooks written with some standards. But lets stick to the quick win of scripting something. Remember that you need a premium Firebase account to do this step to be able to call external APIs.



The code here is as simple as writing a handler and registering this handler to a specific intent by matching its name. So the skeleton of my code looks like this:

function nextPrayerTimeHandler(agent) {
    // some code here
    agent.add(`This is something the agent is going to say to the user!`);
}

function specificPrayerTimeHandler(agent) {
    // some code here
    agent.add(`This is something the agent is going to say to the user!`);
}

intentMap.set('NextPrayerIntent', nextPrayerTimeHandler);
intentMap.set('SpecificPrayerIntent', specificPrayerTimeHandler);


This skeleton blends with the existing boilerplate in the inline editor. The content of my functions is where magic happens. I wrote some quick and dirty code based on the first API I found in my search for prayer times APIs. And spent a good amount of time fiddling with the rabbit hole of JS date objects and local vs UTC mess.

Here is my code if you are curious or want to make your own cleaner code :)

const https = require("https");
const apiBaseTodayPrayers = 'https://api.pray.zone/v2/times/today.json?higher-latitudes=3&school=3&city=';
const prayerKeys = ['Fajr', 'Dhuhr', 'Asr', 'Maghrib', 'Isha'];
var city = '';
var prayer = '';

function constructTimestampFromData(date, time, offset) {
    var offsetSign = (offset > 0) ? '+' : '-';

    var fullTimeString = `${date} ${time} GMT${offsetSign}${offset}`;

    return (new Date(fullTimeString).getTime());
}

function findNextPrayerTimeFromJsonResponse(jsonData) {
    let allPrayerTimes = jsonData.results.datetime[0].times;
    let date = jsonData.results.datetime[0].date.gregorian;
    let offset = jsonData.results.location.local_offset;

    // Filter prayer times and convert from string to timestamp
    var filteredPrayerTimes = {};

    for (var timeKey in allPrayerTimes) {
      if (prayerKeys.includes(timeKey)) {
        filteredPrayerTimes[timeKey] = constructTimestampFromData(date, allPrayerTimes[timeKey], offset);
      }
    }

    // Get local time
    let now = new Date().getTime();

    // Find the next prayer
    var nextPrayerName = '';
    var nextPrayerTime = '';

    for (var key in filteredPrayerTimes) {
      if (filteredPrayerTimes[key] > now) {
        return [key, allPrayerTimes[key]];
      }
    }
}
 
function nextPrayerTimeHandler(agent) {   
    // Get the current city
    city = agent.parameters.city;
   
    // Make API call to fetch prayer times
    const todayPrayersUrl = apiBaseTodayPrayers + city;
    return new Promise((resolve, reject) => {
      https.get(todayPrayersUrl, function(resp) {
        var json = "";
        resp.on("data", function(chunk) {
          json += chunk;
        });

        resp.on("end", function() {
          let jsonData = JSON.parse(json);
          let nextPrayerNameAndTime = findNextPrayerTimeFromJsonResponse(jsonData);

          // Respond
          if (nextPrayerNameAndTime[0] === '') {
            agent.add(`There are no more prayers in ${city} today.`);
          } else {
            agent.add(`The next prayer in ${city} is ${nextPrayerNameAndTime[0]} at ${nextPrayerNameAndTime[1]}.`);
          }

          resolve();
        });
      });
    });
}
 
function specificPrayerTimeHandler(agent) {   
    // Get the current city
    city = agent.parameters.city;
    
    // Get the prayer name
    prayer = agent.parameters.prayer;
    
    const todayPrayersUrl = apiBaseTodayPrayers + city;
    return new Promise((resolve, reject) => {
      https.get(todayPrayersUrl, function(resp) {
        var json = "";
        resp.on("data", function(chunk) {
          json += chunk;
        });

        resp.on("end", function() {
          let jsonData = JSON.parse(json);
          let prayerTime = jsonData.results.datetime[0].times[prayer];

          // Respond
          if (prayerTime === undefined) {
            agent.add(`Sorry, I cound not find ${prayer} time for ${city}.`);
          } else {
            agent.add(`${prayer} in ${city} is at ${prayerTime}.`);
          }

          resolve();
        });
      });
    });
}
 

intentMap.set('NextPrayerIntent', nextPrayerTimeHandler);
intentMap.set('SpecificPrayerIntent', specificPrayerTimeHandler);


6 - Testing with Google Assistant


On the right side of the screen, there is a message saying;
See how it works in Google Assistant.
Click it and you will be directed to Actions Console where you can test against a simulator for Google Assistant as well as other Google devices. You can also test on your own Google Home if you are connected to the same account from Google Home.



And from the Actions Console you can modify some settings and deploy your app to alpha and beta versions, or even submit it to be reviewed and publicly published.

Live demo


That's it! I created a simple app to help me with my daily need. And I learned something new along the way. I have more plans to improve my project to my needs and maybe publish it later to others.

I hope you learned something from my post. :)




Resources


Wednesday, April 1, 2020

Retry ruby code with exceptions multiple times

This problem I faced is pretty simple. A method calling an external API or resource which might throw a random error or fail intermittently. So I want to simple retry because this error is ignorable if it happens one or two times.

Now the standard ruby's retry and redo keywords are not helping here. Because the retry will keep retrying endlessly and I want to raise the error after a few retries, and the redo requires a flag to make it stop. So my solution to retry my ruby code for a few times if a specific exception happens was as simple as a method with the retry logic and accepting a block to keep my code cleaner. And whenever I need to retry a not-so-reliable API, I wrap my code in this method as a block and choose the exception to retry when it happens and how many times to retry.

# whatever method I am using
def do_something_with_tolerance_to_errors
  response = with_error_retry(WhateverException, 2) do
    # my code goes here
  end
end

# my retry handler
def with_error_retry(error_class, retries_count)
  yield
rescue error_class => e
  @retries ||= 0
  raise e if @retries >= retries_count

  @retries += 1
  retry
end