Smart Home Routines

Cover Image for Smart Home Routines

Introduction

I have a Node.js IoT tool that I run for testing out different local APIs for controlling smart home appliances. In this post, I will go through the design I am planning for supporting routines in that tool.

What is a routine?

A routine is a sequence of 1 or more activities that are performed when some predefined trigger occurs. For example, my morning routine consists of a number of activities that I permorm in order, and that routine's trigger is my alarm clock.

A routines trigger can either be scheduled, like my morning routine, or it can be relative to some other non-temporal event. For example, I could have a routine that says whenever the lights are turned off, I want the shades to open. In this case, the trigger is the lights turning off. I will call this a relative trigger and a time based trigger will be referred to as a scheduled trigger.

Relative Trigger

To support a relative trigger, we need:

  1. An event bus to notify when something happens
  2. Rules to determine whether an event triggers a routine

Event Bus

To start with, I am defining the following event types. Note: I refer to smart home entities in my tool as things because it is the internet of things.

enum EventType {
    Action = 'Action',
    StateChange = 'StateChange',
}

interface ActionEvent {
    // The type of event
    type: EventType.Action

    // Id of the thing that was targeted with an action
    thingId: string

    // The action that targeted the thing
    // Typed as any because I do not yet have action types defined.
    // This will likely be a union type, once that has been done.
    action: any
}

interface StateChangeEvent {
    // The type of event
    type: EventType.StateChange

    // Id of the thing that changed state
    thingId: string;

    // The previous state of the thing
    // Typed as any because I do not yet have state types defined.
    // This will likely be a union type, once that has been done.
    previousState: any

    // The current state of the thing
    // Typed as any because I do not yet have state types defined.
    // This will likely be a union type, once that has been done.
    currentState: any
}

Then there is the event bus that processes these events and routes them to interested listeners.

interface ActionEventListener {
    type: EventType.Action
    listener: (event: ActionEvent) => void
}

interface StateChangeEventListener {
    type: EventType.StateChange
    listener: (event: StateChangeEvent) => void
}

interface ThingEventBus {
    /**
     * Notifies the event bus of an event that occurred.
     * @param event The event that occurred
     */
    notify(event: ActionEvent | StateChangeEvent): void

    /**
     * Registers a listener to be called when a particular type of event occurs.
     * @param listener The type of event to listen for and the function to execute when it occurs
     */
    addListener(listener: ActionEventListener | StateChangeEventListener): void

    /**
     * Removes a listener from the event bus. This uses referential equality,
     * so the passed in listener must be the same object that was passed in to addListener.
     * @param listener The listener to be removed from the event bus
     */
     removeListener(listener: ActionEventListener | StateChangeEventListener): void
}

Rules

The next thing we need is a way to register and evaluate rules so that we know when an event is supposed to trigger a routine. To support this, we can introduce a rule engine component.

enum RuleType {
    ActionRule = 'ActionRule',
    StateChangeRule = 'StateChangeRule',
}

interface Rule {
    // The thing this rule applies to.
    thingId: string

    // THe type of rule. This is used to narrow down what rules to evaluate during rule evaluation.
    type: RuleType

    // A JSON Schema condition used to match against the context passed in during rule evaluation.
    // The schema validation is using https://www.npmjs.com/package/jsonschema
    condition: Schema

    // The action to perform when this rule evaluates to true.
    // The context used during rule evaluation is passed through to this function.
    action: (context: any) => void
}

interface ThingRuleEngine {
    /**
     * Adds a rule to the rule engine.
     * @param rule - The rule to be added
     */
    addRule(rule: Rule): void

    /**
     * Removes a rule from the rule engine. This uses referential equality,
     * so the passed in rule must be the same object that was passed in to addRule.
     * @param rule - The rule to be removed
     */
    removeRule(rule: Rule): void

    /**
     * Evaluates all rules registered for the provided thingId and rule type,
     * using the provided context. All rules evaluating to true will have their
     * action executed.
     * @param thingId - Only evaluate rules that were added with this thingId
     * @param ruleType - The type of rule to be evaluated
     * @param context - The context used to evaluate rule conditions
     */
    evaluateRule(thingId: string, type: RuleType, context: any): void
}

The rule engine allows us to add a rule for a specific thing that will trigger a predefined action when it evaluates to true. That action could be a routine. I have added 2 types of rules to further partition what rules need to be evaluated at a given time.

For example:

  1. An ActionEvent would be configured to trigger evaluation of ActionRules
  2. A StateChangeEvent would be configured to trigger evaluation of StateChangeRules

I have chosen to use JSON Schema as the rule condition format, which will be evaluated against the context passed in during evaluateRule. I really only need to check for some state or action object structure to decide whether to trigger a routine, so JSON Schema is a flexible way to write a condition that can be 'matched' against one of those objects.

For example, the following JSON schema would evaluate to true when evaluated with a turnOn action.

// JSON Schema
{
    "type": "object",
    "properties": {
        "action": {"type": "string", "const": "turnOn" }
    }
}

// TurnOn Action
{
    "action": "turnOn"
}

Scheduled Trigger

To support a scheduled trigger, we need:

  1. A scheduler
  2. A new rule: StateRule

Scheduler

A scheduler component allows us to schedule jobs that execute at absolute or relative time. An absolute time can be represented by an ISO 8601 DateTime, and a relative time can be represented by an IOS 8601 Duration.

interface Scheduler {
    /**
     * Schedules a job for execution on a given schedule.
     * @param schedule - Defines the schedule of when the job should execute. 
     *                   This can be either an ISO 8601 DateTime (https://tc39.es/ecma262/#sec-date-time-string-format)
     *                   or an ISO 8601 Duration (https://tc39.es/proposal-temporal/docs/duration.html).
     * @param repeat - An optional number for how many times to repeat the schedule.
     *                 If this is not set then it repeats indefinitely.
     * @param job - The function to execute on the given schedule
     * @returns - The id of the scheduled job
     */
    scheduleJob(schedule: string, job: () => void, repeat?: number): number

    /**
     * Removes a job from the scheduler.
     * If no job exists with the given id then this does nothing.
     * @param jobId - Identifies the job to be removed
     */
    removeJob(jobId: number): void

    /**
     * Skips the next scheduled occurrence of a job.
     * If no job exists with the given id then this does nothing.
     * If the job being skipped only has 1 more time to execute then this behaves
     * the same as removeJob.
     * @param jobId - Identifies the job to skip
     */
    skipJob(jobId: number): void
}

StateRule

A StateRule is not technically required, but I would like to be able to conditionally trigger routines on a schedule. For example, I could have a routine that should turn the lights on every morning but only if the shades are down.

The only thing needed to support this is to add one additional rule type, like:

enum RuleType {
    ...
    StateRule = 'StateRule',
}

The difference between a StateRule and StateChangeRule is that a StateRule is evaluated against the current state of a thing, but a StateChangeRule includes the previous and current state and lets you check the difference and occurs only when the thing state changes.

Wrapping Up

This post gave an overview of my design for adding routines into my smart home tool, including the specific components that I will need to write. Once this is implemented, I can write a follow-up post to update anything from this design that did not work as expected and show a demo of routines in action.