Coding Home Automation
Tags: csharp, azure, twilio, serverless
Published: 2020-06-07 18:28:04

Home automation is easier than ever with a plethora of IoT-connected devices enabling everyday appliances in your home to be internet-enabled. When you write a custom piece of integration for your IoT, often times deployment to the cloud becomes an afterthought, only to become a nightmare when you actually want to update it later, and you don't remember where/how you deployed it. With Pulumi, you don't have to worry about that anymore. You can develop your IoT integration app, as well as your program your infrastructure as an app.

## The Garage Door Opener

Most people have an internet hub connected to their automatic garage door opener, which allows them to remotely monitor the garage door as well as open/close it using a mobile app. But what about when you forget to close it and it stays open? Neither the app nor the existing home automation recipes on the home automation website [IFTTT](https://ifttt.com) have a way to remind you that you left it open. To solve this problem I tried _not_ to build something of my own, but instead try to use [Zapier](https://zapier.com), a task automation platform.

{% github praneetloke/GarageDoorMonitor no-readme %} 

## The First Attempt

My first attempt involved using Zapier. It would have worked if there was a way to update a state while waiting for a timer to fire. I used IFTTT to connect the myQ service to fire a Webhook request each time the garage door opened or closed. The webhook receiver was a Zapier webhook "catch" action, which I then connected to a timer delay before sending me a text message via Twilio. It mostly worked, except, if I closed the garage door before the timer fires, there was no way for me to update the state and therefore, cancel sending the text message.

Here's the "zap" I ended up creating:

![Zap!](https://codejournal.files.wordpress.com/2019/09/zap.png?w=1024)
<figcaption>A Zap on Zapier using built-in actions.</figcaption>

## Durable Functions on Azure Functions

Durable Functions is an extension to the already popular Azure Functions platform. This means, you can write functions with an external trigger (HTTP, Queue etc.) and have it trigger an orchestration. Each orchestration instance is automatically tracked by the platform. Checkout the [API reference](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-http-api#http-api-reference) to see what you can control about an orchestration instance.

## Durable Function Types

There are other durable function types, learn more about them [here](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-types-features-overview). The following are just the types used in this project.

### Orchestration Functions

Each function has a trigger type that identifies _how_ that function can be triggered. Orchestration functions are no different. Orchestration functions typically don't do any work other than, you guessed it, orchestrate other functions that do the work.

### Activity Functions

Activity functions are responsible for most of the work in an orchestration. You can make HTTP calls, call other activity functions etc.

### Entity Functions</h3>

Entity functions allow you to represent your orchestration instance with a state. It is up to you on whether each orchestration instance has its own entity or if your state is a singleton. This is controlled by the way that entities are persisted. Each entity is made up of two components:

* An entity name: a name that identifies the type of the entity (for example, "Counter").
* An entity key: a string that uniquely identifies the entity among all other entities of the same name (for example, a GUID).


## IFTTT + Azure Durable Functions + Twilio + Pulumi

This is the high-level view of the solution I finally ended up with.

![Bird's-eye view](https://codejournal.files.wordpress.com/2019/09/high-level-arch.png?w=673)

* IFTTT receives signals from the garage door opener.
* IFTTT then calls the function app.
* The function app waits for couple of minutes and if the door still isn't closed by then, sends a text message using Twilio.

### Function App

The only external trigger in the function app is the HTTP trigger used in [this](https://github.com/praneetloke/GarageDoorMonitor/blob/master/GarageDoorMonitor/main.cs#L119) function. An orchestration instance is created only through the orchestration client, injected into the HTTP-triggered function as a param.

```csharp
[FunctionName("Main")]
public static async Task RunOrchestrator(
    [OrchestrationTrigger] IDurableOrchestrationContext ctx,
    ILogger log)
{
    var delay = Environment.GetEnvironmentVariable("TimerDelayMinutes");
    var delayTimeSpan = TimeSpan.FromMinutes(Convert.ToInt32(delay));

    var maxRetries = 10;
    var count = 0;
    // The following loop will execute for as long as the garage door remains open
    // up to max number of retries.
    while(true)
    {
        DateTime timer = ctx.CurrentUtcDateTime.Add(delayTimeSpan);
        log.LogInformation($"Setting timer to expire at {timer.ToLocalTime().ToString()}");
        await ctx.CreateTimer(timer, CancellationToken.None);
        log.LogInformation("Timer fired!");

        try
        {
            using (await ctx.LockAsync(EntityId))
            {
                log.LogInformation("Entity lock acquired.");
                var currentState = await ctx.CallEntityAsync<string>(EntityId, "read", null);
                log.LogInformation($"Current state is {currentState}.");
                // If the door is closed already, then don't do anything.
                if (currentState.ToLowerInvariant() == "closed")
                {
                    log.LogInformation("Looks like the door was already closed. Will skip sending text message.");
                    break;
                }
                    await ctx.CallActivityAsync("SendTextMessage", null);
                }
            }
        catch (LockingRulesViolationException ex)
        {
            log.LogError(ex, "Failed to lock/call the entity.");
        }
        catch (Exception ex)
        {
            log.LogError(ex, "Unexpected exception occurred.");
        }

        if (count >= maxRetries)
        {
            log.LogInformation("Reached max retry count, but the garage door is still open. Will stop checking now.");
            break;
        }
        log.LogInformation("Door is still open. Will schedule another timer to check again.");
        count++;
    }
}
```

### Deploying the Infrastructure using Pulumi

We will use Pulumi to deploy our function app. The Pulumi app creates the function app along with the Key Vault containing the Twilio account token necessary for the API call to send a text message. For more information, see the README file in the [`infrastructure`](https://github.com/praneetloke/GarageDoorMonitor/tree/master/infrastructure) folder of the source repo.

![Infrastructure](https://codejournal.files.wordpress.com/2019/09/pulumi-app.png?w=344)

Once the Pulumi app is deployed, you can get the URL for your function app, in order to complete the IFTTT Applet creation in the next step.

### IFTTT Applets

IFTTT allows you to create custom applets, which is basically creating your own recipe of "this" and "that". To create a new applet, click your avatar in the top-right corner on https://ifttt.com, and click **Create**.

![IFTTT Applets](https://codejournal.files.wordpress.com/2019/09/ifttt-create-applet-1.png?w=329)

Click on **+ This** and choose the service called **myQ**. Most of the garage door openers here in the USA are made by the [Chamberlain Group](https://chamberlaingroup.com/) anyway and you are most likely using one of those. All of those openers work with the [myQ](https://www.myq.com/) internet gateway. Your alternative would be to buy a myQ Smart Hub.

![Applet](https://codejournal.files.wordpress.com/2019/09/ifttt-myq.png?w=1024)

Click **+ That** and search for **Webhook** to select it. You will need the URL of the Function App that was deployed using Pulumi. You can get this URL by navigating to https://portal.azure.com, too. Since the infrastructure was deployed using Pulumi, we can easily fetch its output by running `pulumi stack output webhookUrl` in the `infrastructure` folder. We can now complete the Webhook action's configuration in IFTTT.

**Note:** Since the function app is exposed to the internet, we don't want just about anyone to be able to call it. Instead, we will use the built-in function app [authorization keys](https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook#authorization-keys) to allow only IFTTT to invoke it. Any other caller without the function key will receive a **401 Unauthorized** error due to [this auth requirement](https://github.com/praneetloke/GarageDoorMonitor/blob/master/GarageDoorMonitor/main.cs#L120).

![Complete Applet](https://codejournal.files.wordpress.com/2019/09/ifttt-webhook.png?w=833)
<figcaption>Completing the IFTTT applet creation for webhook action.</figcaption>

### Twilio

In order to send a text message, create an account on [Twilio](https://www.twilio.com) and purchase a [Programmable SMS](https://www.twilio.com/console/sms/dashboard) number. Your account SID and token can be found on the dashboard page of the [Twilio Console](https://www.twilio.com/console) or on the [Settings](https://www.twilio.com/console/project/settings) page under the **API Credentials** section.

## Final Notes

A few things important things to note:

* The [KeyVault](https://docs.microsoft.com/en-us/azure/key-vault/general/overview) in the infrastructure is not necessary for a project like this, but it is very easy to create one with Pulumi. And with Azure's new Managed Identity, it is even easier to configure application access to secrets.
* To learn more about security best practices on Azure, read [this](https://www.pulumi.com/blog/7-ways-to-deal-with-application-secrets-in-azure/) excellent post by [Mikhail Shilkov](https://mikhail.io/).

In the next post, we will take a closer look at the Pulumi app used to deploy the Azure Function App.