Build a Smart Light with Azure IoT Hub


点击此处查看中文版本

In this post, we will build a voice control smart light with Azure IoT hub. All hardwares we need are shown in the picture below, a Raspberry Pi, 3 LEDs, 3 220Ω resistances, a breadboard, some DuPont lines, and Amazon Echo Dot.

All hardwares we need

Don’t worry if you do not have one of them or even any of them, we still can make things work with simulated device, and I will explain how to do that and the end of this post.

Before we begin this awesome job, let’s make it clear how does the entire flow of the information go from your voice to the light turn off or on status. Firstly, Amazon Echo Dot records your voice and sends it to Amazon Cloud, then Amazon Cloud transforms your voice to command. After that, Amazon Cloud sends the command to Azure IoT Hub Server side, and Azure IoT Hub Server passes it to Azure IoT Hub Client side. In this post the client is a Raspberry Pi. Finally, Raspberry Pi turns off or on the LED via GPIO pins.

First of all, you need to setup Azure IoT Hub. Azure IoT Hub provides FREE plan, so you needn’t pay for it now. Follow the step in https://github.com/Azure/azure-iot-sdks/blob/master/doc/setup_iothub.md. Notice, if you want to use Azure IoT Hub free plan, you need change Pricing and scale tier from S1 – Standard to F1 – Free. The free plan has a limitation of 8000 messages per day, but it’s enough for our experiment, right?

After setup Azure IoT Hub, we need create a device in our hub. You can follow the steps in https://github.com/Azure/azure-iot-sdks/blob/e1c8c6df558823f21bd94875d940cdb864b490b8/doc/manage_iot_hub.md to create your device. Remember the name of device you created, we’ll need it in the later steps.

Now let’s deploy Azure IoT Hub to the server side. Choose a server that supports Node, for example, Azure web app. Azure web app is not necessary for Azure IoT Hub, if you already have a web hosting with Node support, just use it.

I use express generator to create a node website quickly, you may also use tools you like. Install express generator by simply run

npm install -g generator-express

If it doesn’t work, try run it as admin

sudo npm install -g generator-express

Enter local Azure IoT Hub server development root path, run

yo express

Choose the below options:

? Would you like to create a new directory for your project? Yes
? Enter directory name {appname}
? Select a version to install: MVC
? Select a view engine to use: Jade
? Select a css preprocessor to use (Sass Requires Ruby): None
? Select a database to use: None
? Select a build tool to use: Grunt

After that, you can see a folder named {appname} in your current path, enter it and add two lines in package.json in dependencies field:

"azure-iothub": "^1.0.18",
"azure-event-hubs": "^0.0.4",
"uuid": "^2.0.3"

Then run npm install to apply the changes. Run npm start, and open http://127.0.0.1:3000, you can see a web page shows Generator-Express MVC.

Generator Express default homepage

You have completed local develop environment, great job! Now let’s write the core code of the server side.

Go to controllers folder in app folder, you can see a file named home.js, right? It’s the router of your website. Let’s add a path named /api/smarthome with these code:

router.get('/api/smarthome', function(req, res, next) {
    res.header('Content-Type', 'application/json');
    res.header('Access-Control-Allow-Origin', '*');
    res.render('json', {
        json: {message: 'foo'},
        layout: false
    });
});

Then go back to app folder, and go to views folder, create a file named json.jade, and write this line code in it:

!=JSON.stringify(json)

Now restart your website, and visit http://127.0.0.1:3000/api/smarthome. You can press Ctrl+C to stop the server, and run npm start to start it again.

Smart Home API Router

You can see a JSON in the page, right? Congratulations! You have created an API router in your website!

Next, we need to make it a real API. Go to models folder under app folder, create a file named iot-hub.js. In this file, we need use Azure IoT Hub module.

'use strict';

var IoTHubClient = require('azure-iothub').Client;
var Message = require('azure-iot-common').Message;

var targetDevice = '[Target Device Id]';
var connectionString = '[Connection String]';

var iotHubClient = IoTHubClient.fromConnectionString(connectionString);

function sendC2DMessage(msg, targetDevice) {
    targetDevice = targetDevice || 'Chrome';
    iotHubClient.open(function (err) {
        if (err) {
            console.error('Could not connect: ' + err.message);
        } else {
            console.log('Client connected');

            // Create a message and send it to the IoT Hub
            var data = JSON.stringify({ message : msg });
            var message = new Message(data);
            console.log('Sending message: ' + message.getData());
            iotHubClient.send(targetDevice, message);
        }
    });
}

module.exports = sendC2DMessage;

Replace [Target Device Id] with your device id that you created in your hub. We did it at the beginning of this post, remember? Replace [Connection String] with your own device connection string. The connection string is something like

HostName=xxx.azure-devices.net;SharedAccessKeyName=iothubowner;SharedAccessKey=...

Now we have the IoT Hub model for our web and we can use it in our app. Create a new model named smarthome-turnonoff.js, and write a function to handle turn on off command:

function TurnOnOff(applianceId, requestType) {
    var sendC2DMessage = require('./iot-hub.js')
    new sendC2DMessage({
        applianceId: applianceId,
        request: requestType
    }, '[Target Device Id]');
    var result = {
        applianceId: applianceId,
        request: requestType
    };
    
    return result;
}

module.exports = TurnOnOff;

Also, remember to change [Target Device Id] to your own.

In home.js under controllers folder, change the /api/smarthome router to this:

router.get('/api/smarthome2', function(req, res, next) {
    res.header('Content-Type', 'application/json');
    res.header('Access-Control-Allow-Origin', '*');
    switch(req.query.request) {
        case 'TurnOnRequest' :
            res.render('json', {
                json: new TurnOnOff(req.query.applianceId, 'TurnOn'),
                layout: false
            });
            break;
        case 'TurnOffRequest' :
            res.render('json', {
                json: new TurnOnOff(req.query.applianceId, 'TurnOff'),
                layout: false
            });
            break;
    }
});

And add var TurnOnOff = require('../models/smarthome-turnonoff.js'); at the top of the file.

Restart your website, and visit http://127.0.0.1:3000/api/smarthome?request=TurnOnRequest&applianceId=34f8d140-0704-4d2a-b449-bf2c458afa0a, you can see a JSON shows the id and request your sent, go to Azure portal, open IoT Hub dashboard, you can see you have already sent a message in overview tab.

IoT Hub Dashboard

Now let’s setup device side, Raspberry Pi.

First, We need setup Azure IoT Hub develop environment. Clone Azure IoT SDK repo from https://github.com/Azure/azure-iot-sdks. Cause the repo contains submodules, remember to clone with recursive command like this:

git clone --recursive https://github.com/Azure/azure-iot-sdks.git

Follow the step in https://github.com/Azure/azure-iot-sdks/blob/e1c8c6df558823f21bd94875d940cdb864b490b8/doc/get_started/python-devbox-setup.md.

After complete develop environment setup, create a file named iothub.py in your Raspberry Pi. Remember to copy iothub_client.so to the same path of the file you just created. In the file, write these code:

import iothub_client
from iothub_client import *
from time import sleep
import json

timeout = 241000
minimum_polling_time = 9
receive_context = 0
connection_string = '[Connection String]'

def receive_message(message, counter):
  buffer = message.get_bytearray()
  size = len(buffer)
  message = json.loads(buffer[:size].decode('utf-8')).get('message')
  print("Received Data: <%s>" % message)
  return IoTHubMessageDispositionResult.ACCEPTED

def iothub_init():
  iotHubClient = IoTHubClient(connection_string, IoTHubTransportProvider.HTTP)
  iotHubClient.set_option("timeout", timeout)
  iotHubClient.set_option("MinimumPollingTime", minimum_polling_time)
  iotHubClient.set_message_callback(receive_message, receive_context)
  while True:
    sleep(10)

if __name__ == '__main__':
  iotHubClient = iothub_init()

Again, replace [Connection String] with your own. At this place, the connection string is for device, so it should looks like

HostName=xxx.azure-devices.net;DeviceId=xxx;SharedAccessKey=...'

See? you can find a DeviceId parameter in the connection string, such connection string can be found under Devices tab in Azure portal.

Device Connection String

Let’s run the script, and visit our website again. See, the request has already be received by Raspberry Pi!

Terminal

Then we need make Raspberry Pi control the LEDs. Here’s how I connect LEDs and resistances, DuPont lines on the breadboard.

LED board

The blue line is GND known as ground, and the other three lines, white, gray and purple are light control. We need connect them to the GPIO pins on Raspberry Pi. I have drawn a picture to explain how to connect the lines to make you clear.

RPi GPIO

Now we need a python module called RPi.GPIO, you can download it from https://pypi.python.org/pypi/RPi.GPIO. After download RPi.GPIO, extend it and run setup.py.

Let’s create a script to test LED control via GPIO. Create a file called led.py, and write these code into it:

import RPi.GPIO as GPIO
from time import sleep

GPIO.setmode(GPIO.BCM)
GPIO.cleanup(17)
GPIO.cleanup(27)
GPIO.cleanup(22)
GPIO.setup(17, GPIO.OUT)
GPIO.setup(27, GPIO.OUT)
GPIO.setup(22, GPIO.OUT)
GPIO.output(17, False)
GPIO.output(27, False)
GPIO.output(22, False)
while True:
  GPIO.output(17, True)
  sleep(1)
  GPIO.output(17, False)
  GPIO.output(27, True)
  sleep(1)
  GPIO.output(27, False)
  GPIO.output(22, True)
  sleep(1)
  GPIO.output(22, False)

Now you should see the LEDs on and off one by one.

Great! You can control the hardware with code! Cool, uh?

Next, let’s combine the two script we wrote, iothub.py and led.py. Name the new script with a cool thing, for example, smartlight.py. Write these code into it:

import iothub_client
from iothub_client import *
from time import sleep
import json
import RPi.GPIO as GPIO

green_led_id = 'db87ffe4-5d5d-4af7-bb70-da8a43beac90'
red_led_id = '1266ab90-b23d-4e0f-83d0-ec162284952f'
yellow_led_id = '7b38a9f2-c9f4-42cf-bb63-59147eb685b4'

led_gpio = {green_led_id: 22, red_led_id: 27, yellow_led_id: 17}

GPIO.setmode(GPIO.BCM)
GPIO.setup(led_gpio[green_led_id], GPIO.OUT)
GPIO.setup(led_gpio[red_led_id], GPIO.OUT)
GPIO.setup(led_gpio[yellow_led_id], GPIO.OUT)
GPIO.output(led_gpio[green_led_id], False)
GPIO.output(led_gpio[red_led_id], False)
GPIO.output(led_gpio[yellow_led_id], False)

timeout = 241000
minimum_polling_time = 9
receive_context = 0
connection_string = '[Connection String]'

def receive_message(message, counter):
  buffer = message.get_bytearray()
  size = len(buffer)
  message = json.loads(buffer[:size].decode('utf-8')).get('message')
  print("ID: %s\nRequest: %s" % (message['applianceId'], message['request']))
  if message['request'] == 'TurnOn':
    GPIO.output(led_gpio[message['applianceId']], True)
  elif message['request'] == 'TurnOff':
    GPIO.output(led_gpio[message['applianceId']], False)
  return IoTHubMessageDispositionResult.ACCEPTED

def iothub_init():
  iotHubClient = IoTHubClient(connection_string, IoTHubTransportProvider.AMQP)
#  iotHubClient.set_option("timeout", timeout)
#  iotHubClient.set_option("MinimumPollingTime", minimum_polling_time)
  iotHubClient.set_message_callback(receive_message, receive_context)
  while True:
    sleep(10)

if __name__ == '__main__':
  iotHubClient = iothub_init()

Replace '[Connection String]' with your own. Am I tooooo nagging? If so, sorry about that.

OK, magic things will happen! Run the script in Raspberry Pi, restart your website, visit http://127.0.0.1:3000/api/smarthome2?request=TurnOnRequest&applianceId=1266ab90-b23d-4e0f-83d0-ec162284952f.

See what! The red LED is on, right! It is amazing! Now you can control the LED from HTTP request!

Of course, this is not a complete smart light, we should make control interface more friendly. Next we will add Amazon Echo Dot in!

You can learn how to create an Alexa Smart Home Skill from https://developer.amazon.com/public/community/post/Tx4WG410EHXIYQ/Five-Steps-Before-Developing-a-Smart-Home-Skill.

To add LEDs into your Alexa console, we need use Alexa Smart Home Skill discovery request. And to control the LEDs, we need control request.

Here’s the smart home skill Lambda function I wrote:

var https = require('https');
var REMOTE_CLOUD_BASE_PATH = '/api/smarthome';
var REMOTE_CLOUD_HOSTNAME = '[Cloud Hostname]';

exports.handler = function(event, context) {

    log('Input', event);

    try{
        switch (event.header.namespace) {
            case 'Alexa.ConnectedHome.Discovery':
                handleDiscovery(event, context);
                break;
            case 'Alexa.ConnectedHome.Control':
                handleControl(event, context);
                break;
            default:
                log('Err', 'No supported namespace: ' + event.header.namespace);
                context.fail('Something went wrong');
                break;
        }
    }
    catch(e) {
        log('error', e);
    }
};

function handleDiscovery(event, context) {
    log('Discovery', event);
    var basePath = '';
    basePath = REMOTE_CLOUD_BASE_PATH + '?request=Discovery';
    var options = {
        hostname: REMOTE_CLOUD_HOSTNAME,
        port: 443,
        path: basePath,
        headers: {
            accept: 'application/json'
        }
    };

    log('Discovery', options);
    
    var serverError = function (e) {
        log('Error', e.message);
        context.fail(generateControlError(event.header.name,
            'DEPENDENT_SERVICE_UNAVAILABLE',
            'Unable to connect to server'));
    };

    var callback = function(response) {
        log('Discovery Get', response);
        var str = '';

        response.on('data', function(chunk) {
            str += chunk.toString('utf-8');
            log('Discovery Data', str);
        });

        response.on('end', function() {
            log('Result', str);
            var result = JSON.parse(str);

            context.succeed(result);
            log('Result', result);
        });

        response.on('error', serverError);
    };

    https.get(options, callback).on('error', serverError).end();

    log('Discovery Got', 'Got');
}

function handleControl(event, context) {

    if (event.header.namespace === 'Alexa.ConnectedHome.Control') {

        /**
         * Retrieve the appliance id and accessToken from the incoming message.
         */
        var applianceId = event.payload.appliance.applianceId;
        var accessToken = event.payload.accessToken.trim();
        log('applianceId', applianceId);

        var basePath = '';
        basePath = REMOTE_CLOUD_BASE_PATH + '?applianceId=' + applianceId +
            '&request=' + event.header.name;

        var options = {
            hostname: REMOTE_CLOUD_HOSTNAME,
            port: 443,
            path: basePath,
            headers: {
                accept: '*/*'
            }
        };

        var serverError = function (e) {
            log('Error', e.message);
            context.fail(generateControlError(event.header.name,
                'DEPENDENT_SERVICE_UNAVAILABLE',
                'Unable to connect to server'));
        };

        var callback = function(response) {
            var str = '';

            response.on('data', function(chunk) {
                str += chunk.toString('utf-8');
            });

            response.on('end', function() {
                log('done with result');
                var headers = {
                    namespace: 'Alexa.ConnectedHome.Control',
                    name: event.header.name.replace('Request', 'Confirmation'),
                    payloadVersion: '1'
                };
                var payloads = {
                    success: true
                };
                var result = {
                    header: headers,
                    payload: payloads
                };
                log('Done with result', result);
                context.succeed(result);
            });

            response.on('error', serverError);
        };

        https.get(options, callback)
            .on('error', serverError).end();
    }
}

function log(title, msg) {
    console.log('*************** ' + title + ' *************');
    console.log(msg);
    console.log('*************** ' + title + ' End*************');
}

function generateControlError(name, code, description) {
    var headers = {
        namespace: 'Alexa.ConnectedHome.Control',
        name: name,
        payloadVersion: '1'
    };

    var payload = {
        exception: {
            code: code,
            description: description
        }
    };

    var result = {
        header: headers,
        payload: payload
    };

    return result;
}

Replace [Cloud Hostname] with your own. That hostname is just of the node website you developed in local. Do not use 127.0.0.1, it doesn’t work for Lambda, you need publish it to the Internet. I use an SSL connection between Lambda and Azure web app, if you use Azure, you can just use [your-app-name].azurewebsites.net, Azure supports SSL. If you use your own domain, you need an SSL certificate. You can get a free SSL certificate for you domain from https://ssl.md.

Echo Dot cannot work correctly with our smart light currently, because Echo Dot cannot understand the callback message we give from the website, also, we have done nothing about the discovery command.

OK, let’s make some change with our website. Now go to app/models of your website, create 3 new files named smarthome-discovery.js, smarthome-adddevice.js and smarthome-removedevice.js.

In smarthome-discovery.js, write these code:

function Discovery() {
    var headers = {
        namespace: 'Alexa.ConnectedHome.Discovery',
        name: 'DiscoverAppliancesResponse',
        payloadVersion: '1'
    };

    var payloads = {
        discoveredAppliances: appliances
    };

    var result = {
        header: headers,
        payload: payloads
    };

    return result;
}

module.exports = Discovery;

In smarthome-adddevice.js, write these code:

function AddDevice(manufacturerName, modelName, version, friendlyName, friendlyDescription, actions) {
    if (!friendlyName) {
        return {success: false};
    }
    friendlyName = 'Azure ' + friendlyName;
    var uuid = require('uuid');
    var applianceId = uuid.v4();
    manufacturerName = manufacturerName || friendlyName.replace(/\s+/g, '');
    modelName = modelName || manufacturerName;
    version = version || '1.0';
    friendlyDescription = friendlyDescription || 'No Description';
    actions = actions || ['turnOn', 'turnOff'];
    var appliance = {
        applianceId: applianceId,
        manufacturerName: manufacturerName,
        modelName: modelName,
        version: version,
        friendlyName: friendlyName,
        friendlyDescription: friendlyDescription,
        isReachable: true,
        actions: actions,
        status: 'TurnOff'
    }

    appliances.push(appliance);

    return {success: true};
}

module.exports = AddDevice;

In smarthome-removedevice.js, write these code:

function RemoveDevice(applianceId) {
    var index = -1;
    console.log(appliances)
    appliances.forEach(function(appliance) {
        console.log(appliance.applianceId)
        console.log(applianceId)
        console.log(appliance.applianceId === applianceId)
        index++;
        if (appliance.applianceId === applianceId) {
            appliances.splice(index, 1);
            removed = true;
            return {success: true};
        }
    });
    return {success: false};
}

module.exports = RemoveDevice;

Also, edit home.js in app/controllers folder, add these 3 models at the top of the script:

var Discovery = require('../models/smarthome-discovery.js'),
    AddDevice = require('../models/smarthome-adddevice.js'),
    RemoveDevice = require('../models/smarthome-removedevice.js');

Modify smarthome-turnonoff.js to make Echo Dot understand we have accepted the request it sent:

function TurnOnOff(applianceId, requestType) {
    var headers = {
        namespace: 'Alexa.ConnectedHome.Control',
        name: requestType + 'Confirmation',
        payloadVersion: '1'
    };

    var payloads = {
        success: true
    };
    
    var result = {
        header: headers,
        payload: payloads
    };

    var sendC2DMessage = require('./iot-hub.js')
    new sendC2DMessage({
        applianceId: applianceId,
        request: requestType
    }, 'Alexa');

    appliances.forEach(function(appliance) {
        if (appliance.applianceId === applianceId) {
            appliance.status = requestType;
        }
    });
    
    return result;
}

module.exports = TurnOnOff;
module.exports = TurnOnOff;

And modify /api/smarthome router to

router.get('/api/smarthome', function(req, res, next) {
    res.header('Content-Type', 'application/json');
    res.header('Access-Control-Allow-Origin', '*');
    switch(req.query.request) {
        case 'Discovery' :
            res.render('json', {
                json: new Discovery(),
                layout: false
            });
            break;
        case 'AddDevice' :
            res.render('json', {
                json: new AddDevice(
                        req.query.manufacturerName,
                        req.query.modelName,
                        req.query.version,
                        req.query.friendlyName,
                        req.query.friendlyDescription,
                        req.query.actions
                    ),
                layout: false
            });
            break;
        case 'RemoveDevice' :
            res.render('json', {
                json: new RemoveDevice(req.query.applianceId),
                layout: false
            });
            break;
        case 'TurnOnRequest' :
            res.render('json', {
                json: new TurnOnOff(req.query.applianceId, 'TurnOn'),
                layout: false
            });
            break;
        case 'TurnOffRequest' :
            res.render('json', {
                json: new TurnOnOff(req.query.applianceId, 'TurnOff'),
                layout: false
            });
            break;
    }
});

We use a global variable called appliances to store devices we add, you can use a database to replace it. So we need declare appliances in app.js in the root path of the website:

appliances = [];

Do not use var to declare it as we need it be global.

All things are ready! Let’s test it!

First, visit your website to add a device from https://[your-website-host]/api/smarthome?request=AddDevice&friendlyName=Test+Device, and you’ll see {"success":true}. Go to Alexa dashboard, enter Smart Home tab, click Discovery devices link, or just say Discover to your Echo Dot. After about 20 seconds, you can find the device you just added.

Test Device

Now let add 3 new devices with the above step, and visit https://[your-website-host]/api/smarthome?request=Discovery to find out their appliance ids.

Appliance Id

After find out those ids, change the python script in you Raspberry Pi, replace db87ffe4-5d5d-4af7-bb70-da8a43beac90, 1266ab90-b23d-4e0f-83d0-ec162284952f and 7b38a9f2-c9f4-42cf-bb63-59147eb685b4.

Now, let Echo Dot forget the Test Device, run python script on Raspberry Pi, recover devices again, and ask Alexa to turn on Azure Red Light.

Alexa LED

If you do not have a Raspberry Pi, you can download a Chrome extension called Azure Smart Light Simulator I wrote from https://github.com/Sneezry/Azure-Smart-Light-Simulator.

Light Simulator

Have fun to build your own smart home devices with Azure IoT!

Comments (0)

Skip to main content