Building a Philips Hue integration using sipgate.io

David
11.08.2020 0 7:47 min

In this tutorial

You will learn how to integrate sipgate.io into your smart home using Philips Hue.

See the finished result in our interactive demo.

Set up a project

For this project we will be using the official sipgate.io Node.js library.
It makes working with sipgate’s APIs much easier and also provides a convenient way to set up a webhook server.

But first, let’s create a new Node project.
To do that, create a new directory that will hold our project and inside run npm init -y.

This creates a package.json file containing some meta data for the project.
Open it up and add

"scripts": {
  "start": "node server.js"
}

inside the outer curly braces to register a start script that can later be called from the command line.

That’s it for now.
We will come back and install some dependencies later.

To test and run this project you can use your existing Philips Hue setup.
Alternatively it is possible to simulate the Hue Bridge using an emulator.
If you are using the emulator you will need to replace the usages of createLocal with createInsecureLocal since it doesn’t use https.

Connecting to the Hue bridge

First we have to connect to the Hue bridge with a username.
In our setup the code will look for a file .config.json and use its content to authenticate.
The basic file should look like this:

{
  "bridgeIpAddress": "localhost",
  "bridgePort": 443
}

If you already have a Hue bridge user set up, you can enter its credentials in this configuration too:

{
  "credentials": { "username": "your-username" }
}

Let’s get to our first code snippet, which will check for the existence of the config file mentioned above. Create a file server.js with the following contents:

const path = require("path");
const fs = require("fs");

const configFile = path.resolve(__dirname, ".config.json");

let configuration;

try {
  configuration = JSON.parse(fs.readFileSync(configFile).toString());
} catch (e) {
  console.error(
    "Please provide a .config.json file with an bridgeIpAddress and bridgePort."
  );
  return;
}

For the next code snippet we’ll be using the node-hue-api-library for creating the user and readline-sync for prompting the user to press the button. You can install these dependencies using npm install -S node-hue-api readline-sync.

The constants appName and deviceName are used when creating a new user on the Hue bridge.

// add this where our imports are
const readlineSync = require("readline-sync");

const v3 = require("node-hue-api").v3;
const hueApi = v3.api;
const LightState = v3.lightStates.LightState;

// we use this to create our user on the bridge
const appName = "io-labs-hue-integration";
const deviceName = "call-light";

Next, we need to check whether the configuration object contains the credentials.
If it does, we will simply call our runServer function with them, otherwise we need to ask the user to press the Link Button on their Hue bridge, create a user and write the newly gained credentials back into the config file.

const createUser = async () => {
  const unauthenticatedApi = await hueApi
    // use createInsecureLocal if using the emulator
    .createLocal(configuration.bridgeIpAddress, configuration.bridgePort)
    .connect();

  return await unauthenticatedApi.users.createUser(appName, deviceName);
};

const runServer = (userCredentials) => {
  // TODO: we will implement this later
};

if (!configuration.credentials) {
  console.log("No existing credentials detected.");

  readlineSync.question(
    "Please push the Link button on your Hue Bridge and then press enter to proceed >"
  );
  createUser()
    .then((createdUser) => {
      console.log(`username: ${createdUser.username}`);
      configuration.credentials = createdUser;
      fs.writeFileSync(configFile, JSON.stringify(configuration));
      return createdUser;
    })
    .then(runServer)
    .catch((err) => {
      console.error(err.message);
    });
} else {
  runServer(configuration.credentials).catch(console.error);
}

At this point you should be able to try out the code by running npm start.

Setting up webhooks

To actually make our Hue setup able to handle incoming calls, we will use sipgate.io’s webhook feature.
It allows us to run a server which will receive real time call events.

For that, you need to set up your sipgate account for sipgate.io.
For infos on how to do that, check out www.sipgate.io/#get_started

To activate webhooks for incoming and outgoing calls, go to console.sipgate.com and select „Webhooks“ from the sidebar on the left.
The „URLs“ tab lets you configure target URLs for these webhooks.
It distinguishes incoming and outgoing calls and also allows for the explicit selection of phonelines that should trigger webhooks.
By default all phonelines are activated.

For the purpose of this application we will only be using the „Incoming“ URL.
It can be any publicly accessible URL on the web.
Local addresses like localhost or 127.0.0.1 and any other local network address will not work.

When using webhooks in production you will want to run your code on a proper webserver with a proper address.
However, for the purpose of development we recommend using a service that makes your local environment accessible via the internet.
This makes it much easier to rapidly test out your written code.

There are a various free services that can be used to accomplish this.
Some examples are localhost.run or ngrok.
Either one supplies you with a public URL that you can paste to the sipgate.io console.
Just make sure that you forward the correct port (in this tutorial we’ll be using port 8080) and that the provider that you choose offers secure connections through HTTPS.

Add the sipgate.io library using the command npm install -S sipgateio.

We will use the createWebhookModule function, so add the following code to your import section.

const { createWebhookModule } = require("sipgateio");

Add the webhook URL and port to the configuration file.

{
  // ...
  "webhookURL": "example.com",
  "webhookPort": 8080
}

The following code will the create and run the webhook server.
The webhook server’s port and URL are read from the configuration object.

Using server.onNewCall we listen for new calls, for now just logging the caller’s number.

async function runServer(userCredentials) {
  const webhookServerOptions = {
    port: configuration.webhookPort,
    serverAddress: configuration.webhookURL,
  };
  const server = await createWebhookModule().createServer(webhookServerOptions);
  console.log(`Server running at ${configuration.webhookURL}`);

  server.onNewCall(async (newCallEvent) => {
    console.log(`incoming call from ${newCallEvent.from}`);
  });
}

Connecting the Hue bridge to call events

First we create a few helper functions for connecting to the Hue bridge and blinking some LEDs.

function connectToBridge(ip, port, userCredentials) {
  return hueApi
    // use createInsecureLocal if using the emulator
    .createLocal(ip, port)
    .connect(userCredentials.username);
}

The blinkLight function first sets the light’s color and then simply calls turnOn and turnOff repeatedly, each time delaying for the time specified in the duration parameter.

function turnOff(api, lightId) {
  return api.lights.setLightState(lightId, new LightState().off());
}

function turnOn(api, lightId) {
  return api.lights.setLightState(lightId, new LightState().on());
}

const delay = (duration) =>
  new Promise((resolve) => setTimeout(resolve, duration));

async function blinkLight(api, lightId, duration, count, color) {
  await api.lights.setLightState(lightId, new LightState().on().rgb(color));
  for (let i = 0; i < count; i++) {
    await turnOn(api, lightId);
    await delay(duration);
    await turnOff(api, lightId);
    await delay(duration);
  }
}

Inside our runServer function, we will now connect to the Hue bridge using our credentials.
Then we will get the IDs of all lights in the network so that we can make them blink later.

async function runServer(userCredentials) {
  let bridge;
  try {
    bridge = await connectToBridge(
      configuration.bridgeIpAddress,
      configuration.bridgePort,
      userCredentials
    );
    await bridge.configuration.getConfiguration();
  } catch (e) {
    console.error(e.message);
    return;
  }

  const allLightIds = await bridge.lights.getAll();

  /// ...
}

Now all that’s left to do is calling the previously defined blinkLight function for every light whenever we receive a new call event.

async function runServer(userCredentials) {
  // ...
  server.onNewCall(async (newCallEvent) => {
    console.log(`incoming call from ${newCallEvent.from}`);

    const color = [255, 0, 255];
    await Promise.all(
      allLightIds.map((lightId) => blinkLight(bridge, lightId, 200, 10, color))
    );
  });
}

Promise.all() will make all light blink at the same time, instead of one after the other.

Now you are basically done!

Feel free to try different ways to blink the LEDs or handle different webhook events.

For example, you can have the color of the lamp depend on who is calling.
Just add a map from the caller’s number to the desired color like this:

const defaultColor = [255, 0, 0];

const personColors = {
  "<some phone number>": [255, 0, 255],
};

And decide which color to use by looking up the new caller number in that map.

const color = personColors[newCallEvent.from] || defaultColor;

await Promise.all(
  allLightIds.map((lightId) => blinkLight(bridge, lightId, 200, 10, color))
);

You can check out the whole project here: https://github.com/sipgate-io/io-labs-hue-integration

Keine Kommentare


Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert