Introduction

Hi all! This is my blog post for the Sensing the World Challenge. I would like to wish the best of luck to everyone participating in this challenge!

I was always carious how much energy my electrical devices consume, for instance a Mac charger or a fun. It can be really hot in the summer in Poland so the fun was turned on for the whole night. But how much energy it used during the night? So here it is an idea - let's build a device that can measure the energy consumption. It would be also nice to have a possibility to control the connected electric device - turn it on and off.

Devices

devices

This is a list of devices I will use to build MeasureBridge.

  1. Azure sphere MT3620 Starter Kit
  2. AC Current click board + AC Sensor
  3. RELAY click board
  4. OLED 0.96" I2C 128x64 display
  5. Power supply - Hi-Link HLK-PM01 100V-240VAC / 5VDC - 0,6A (not included in that picture)

 

 

Concept and goals

 

The concept behind this project is having a small device (later called MeasureBridge) that can be used to measure consumed energy. The user can connect an electric device to the MeasureBridge and then MeasureBridge to the power.

I have a couple of goals I would like to achieve:

  • Measure the energy consumption and store it in the cloud
  • Display the measured consumption on the OLED display and also some calculated values like average usage for the past month etc.
  • Control the attached electric device (turn it on and off) with my phone
  • Put it all in nice wooden box

 

As you saw in the device list, I want to use MikroE Click boards to measure the current and also to control the connected device.

Design

 

Architecture and project structure

 

As you can see below there is a project structure. Starting from the top there is a HomeBridge server installed on RaspberryPi. It's a gateway between iPhone and Microsoft Azure. More about HomeBridge later on.

Next there are services from Microsoft Azure:

  • Azure IoT Hub - used to communicate with Azure Sphere
  • Azure Functions - used to
    • capture messages send from Azure Sphere device
    • calculate energy consumption and send it back to Azure Sphere
  • Azure Table Storage - used to store data from Azure Sphere and Azure Functions

 

The last but not least we have Azure Sphere device (Azure sphere MT3620 Starter Kit) and MikroE Click Boards.

 

MeasureBridge - design

Circuit diagram

 

{gallery} Circuit diagram

proto

Breadboard schematics

Connection schematics

 

Build

 

In this section I will describe each of the devices and how I put them all together.

Source code: https://github.com/paw3lx/MeasureBridge

 

Azure Sphere

 

Avnet sphere MT3620 Starter Kit was used as a main board for the project. At the begging I set up the azure sphere module and registered it in the Azure IoT Hub. I will not go into details of that cause it is really well described in other tutorial blog posts. However, that is the list of things I did

  • Install an SDK for Windows
  • Claim device and enable debugging
  • Set up WiFi
  • Create Azure IoT Hub and add a new IoT Device

 

Blog posts that really helped me to get started

 

 

MikroE Click boards were attached into ClickModule 1 and ClickModule 2. However I had some issues connecting Relay Click board to the Click Module 2 - because if SPI or i2c initialization process the relay was switching on and off during startup. It's not safe to immediately turn on and off electric devices so I decided to not plug that module directly. Instead I've used small breadboard and then connect it to the Avnet Sphere.

The display is connected to the OLED connector on the Avnet Sphere board.

proto_1

 

OLED display

 

Product link: https://www.amazon.com/gp/product/B072Q2X2LL/ (mine has blue display)

I used a blue OLED display that is 0.96" wide. It's based on the SSD1306 driver and it supports i2c communication.

There is a good blog post on how to use it here. I used the library from the example (https://github.com/CloudConnectKits/Azure_Sphere_SK_ADC_RTApp/blob/master/AvnetAzureSphereSK_OLED/AvnetStarterKitReferen… ) to communicate with the OLED display. The idea is to display:

  • Current [A]
  • Energy [kWh]
  • State of the relay [On/Off]
  • Time since the relay was turned on

 

You can see below the code responsible for what is displayed on the OLED. It is a state machine which is really easy to control. You have oled_state variable which you need to set to display desirable scene. I decided not to use any buttons in the project. So every couple of seconds I'm increasing the oled_state to switch to the next scene.

At the bottom there are some functions that converts number into string.

 

#include "oled.h"
#include <math.h>


uint8_t oled_state = 0;


float ac_current;
float ac_averageLastHour;
float kwh_today;
float kwh_last_7_days;
float kwh_last_month;
char current_time_buffer[26];
bool relay_1_is_on;


/**
  * @brief  OLED initialization.
  * @param  None.
  * @retval Positive if was unsuccefully, zero if was succefully.
  */
uint8_t oled_init()
{
return sd1306_init();
}


void update_oled()
{
switch (oled_state)
{
case 0:
display_consumption();
break;
case 1:
display_projections();
break;
case 2:
display_time();
break;
case 3:
display_relay_state();
break;
}

}


void update_oled_state(void)
{
if (oled_state < 3)
{
oled_state += 1;
oled_state = oled_state % 3;
}
update_oled();
}


void display_consumption(void)
{
clear_oled_buffer();
sd1306_draw_string(OLED_TITLE_X, OLED_TITLE_Y, "  Current", FONT_SIZE_TITLE, white_pixel);


uint8_t ac_string_data[10];
uint8_t str_label[] = "Current [A]: ";
// Convert x value to string
ftoa(ac_current, ac_string_data, 4);


// Draw a label at line 1
sd1306_draw_string(OLED_LINE_1_X, OLED_LINE_1_Y, str_label, FONT_SIZE_LINE, white_pixel);
// Draw the value of x
sd1306_draw_string(sizeof(str_label) * 6, OLED_LINE_1_Y, ac_string_data, FONT_SIZE_LINE, white_pixel);


uint8_t watts_string_data[10];
uint8_t str_label2[] = "Power   [W]: ";
// Convert x value to string
float wats = ac_current * 230.0;
ftoa(wats, watts_string_data, 1);


// Draw a label at line 1
sd1306_draw_string(OLED_LINE_2_X, OLED_LINE_2_Y, str_label2, FONT_SIZE_LINE, white_pixel);
// Draw the value of x
sd1306_draw_string(sizeof(str_label2) * 6, OLED_LINE_2_Y, watts_string_data, FONT_SIZE_LINE, white_pixel);


uint8_t str_label3[] = "Last h  [A]: ";
uint8_t ac_last_hour_string_data[10];
// Convert x value to string
ftoa(ac_averageLastHour, ac_last_hour_string_data, 2);


// Draw a label at line 1
sd1306_draw_string(OLED_LINE_3_X, OLED_LINE_3_Y, str_label3, FONT_SIZE_LINE, white_pixel);
// Draw the value of x
sd1306_draw_string(sizeof(str_label3) * 6, OLED_LINE_3_Y, ac_last_hour_string_data, FONT_SIZE_LINE, white_pixel);


sd1306_refresh();
}


void display_projections(void)
{
clear_oled_buffer();
sd1306_draw_string(OLED_TITLE_X, OLED_TITLE_Y, "Consumption", FONT_SIZE_TITLE, white_pixel);


uint8_t kwh_today_string_data[10];
uint8_t str_label[] = "Today  [kWh]: ";
// Convert x value to string
ftoa(kwh_today, kwh_today_string_data, 4);


// Draw a label at line 1
sd1306_draw_string(OLED_LINE_1_X, OLED_LINE_1_Y, str_label, FONT_SIZE_LINE, white_pixel);
// Draw the value of x
sd1306_draw_string(sizeof(str_label) * 6, OLED_LINE_1_Y, kwh_today_string_data, FONT_SIZE_LINE, white_pixel);


uint8_t kwh_7days_string_data[10];
uint8_t str_label2[] = "7 days [kWh]: ";
// Convert x value to string
ftoa(kwh_last_7_days, kwh_7days_string_data, 4);


// Draw a label at line 1
sd1306_draw_string(OLED_LINE_2_X, OLED_LINE_2_Y, str_label2, FONT_SIZE_LINE, white_pixel);
// Draw the value of x
sd1306_draw_string(sizeof(str_label2) * 6, OLED_LINE_2_Y, kwh_7days_string_data, FONT_SIZE_LINE, white_pixel);


uint8_t str_label3[] = "Month  [kWh]: ";
uint8_t kwh_last_month_string_data[10];
// Convert x value to string
ftoa(kwh_last_month, kwh_last_month_string_data, 4);


// Draw a label at line 1
sd1306_draw_string(OLED_LINE_3_X, OLED_LINE_3_Y, str_label3, FONT_SIZE_LINE, white_pixel);
// Draw the value of x
sd1306_draw_string(sizeof(str_label3) * 6, OLED_LINE_3_Y, kwh_last_month_string_data, FONT_SIZE_LINE, white_pixel);


sd1306_refresh();
}


void display_relay_state(void)
{
clear_oled_buffer();
sd1306_draw_string(OLED_TITLE_X, 16, " R E L A Y", FONT_SIZE_TITLE, white_pixel);
if (relay_1_is_on == true) {
sd1306_draw_string(OLED_TITLE_X, 40, "    O N", FONT_SIZE_TITLE, white_pixel);
}
else {
sd1306_draw_string(OLED_TITLE_X, 40, "   O F F", FONT_SIZE_TITLE, white_pixel);
}
sd1306_refresh();
}


void display_time(void)
{
clear_oled_buffer();
if (relay_1_is_on == true) {
sd1306_draw_string(OLED_TITLE_X, OLED_TITLE_Y, "  RELAY ON", FONT_SIZE_TITLE, white_pixel);
uint8_t str_label2[] = "     ";
char elapsedTime[11];
strncpy(elapsedTime, elapsed_time_buffer, 20);
elapsedTime[20] = '\0'; // place the null terminator


// Draw a label at line 1
sd1306_draw_string(OLED_LINE_2_X, OLED_LINE_2_Y, str_label2, FONT_SIZE_LINE, white_pixel);
// Draw the value of x
sd1306_draw_string(sizeof(str_label2) * 6, OLED_LINE_2_Y, elapsedTime, FONT_SIZE_LINE, white_pixel);
}
else {
sd1306_draw_string(OLED_TITLE_X, OLED_TITLE_Y, " RELAY OFF", FONT_SIZE_TITLE, white_pixel);
}



char timeToDisplay[21];
strncpy(timeToDisplay, current_time_buffer, 20);
timeToDisplay[20] = '\0'; // place the null terminator
// Draw a label at line 1
sd1306_draw_string(OLED_LINE_4_X, OLED_LINE_4_Y, timeToDisplay, FONT_SIZE_LINE, white_pixel);






sd1306_refresh();
}


/**
  * @brief  Converts a given integer x to string uint8_t[]
  * @param  n: float number to convert
  * @param  res:
  * @param  afterpoint:
  * @retval None.
  */
void ftoa(float n, uint8_t* res, int32_t afterpoint)
{
// Extract integer part 
int32_t ipart = (int32_t)n;


// Extract floating part 
float fpart = n - (float)ipart;


int32_t i;


if (ipart < 0)
{
res[0] = '-';
res++;
ipart *= -1;
}


if (fpart < 0)
{
fpart *= -1;


if (ipart == 0)
{
res[0] = '-';
res++;
}
}


// convert integer part to string 
i = intToStr(ipart, res, 1);


// check for display option after point 
if (afterpoint != 0)
{
res[i] = '.';  // add dot 


// Get the value of fraction part upto given no. 
// of points after dot. The third parameter is needed 
// to handle cases like 233.007 
fpart = fpart * pow(10, afterpoint);


intToStr((int32_t)fpart, res + i + 1, afterpoint);
}
}


/**
  * @brief  Converts a given integer x to string uint8_t[]
  * @param  x: x integer input
  * @param  str: uint8_t array output
  * @param  d: Number of zeros added
  * @retval i: number of digits
  */
int32_t intToStr(int32_t x, uint8_t str[], int32_t d)
{
int32_t i = 0;
uint8_t flag_neg = 0;


if (x < 0)
{
flag_neg = 1;
x *= -1;
}
while (x)
{
str[i++] = (x % 10) + '0';
x = x / 10;
}


// If number of digits required is more, then 
// add 0s at the beginning 
while (i < d)
{
str[i++] = '0';
}


if (flag_neg)
{
str[i] = '-';
i++;
}


reverse(str, i);
str[i] = '\0';
return i;
}


// reverses a string 'str' of length 'len' 
static void reverse(uint8_t* str, int32_t len)
{
int32_t i = 0;
int32_t j = len - 1;
int32_t temp;


while (i < j)
{
temp = str[i];
str[i] = str[j];
str[j] = temp;
i++; j--;
}
}

 

MikroE Relay click

 

Product link: https://www.mikroe.com/relay-click

Relay Click board offers an elegant and easy way for controlling a wide range of electric devices. There are two relays on the board and each can be in opened or closed state. When the relay is in the closed switch state the current flows through it.

Avnet Sphere supports two MikroE click-boards - ClickModule #1 and #2. As I mentioned at the begging, I've connected the Relay Click to the ClickModule #2, but not directly.

 

relay click

 

Relay Click module is really easy to use. You have two relays that can be controlled. All you need to do is to set hight or a low state on one of the inputs: CS for Relay1 and PWM for Relay2.

 

MikroE AC Current click

 

AC Current click is a device that is able to measure the alternating current (AC) running through the conductor, using the current sensor. It allows measuring up to 30A. The Click board™ comes equipped with the 3.5mm jack connector which is used to attach the sensing probe. You have to put one of the wires through the probe. Sensor outputs 0-1V; 1V for the full-scale reading of 30 A. Then the signal is filtered and amplified. On the board there is also MCP3201, a 12bit SAR analog-to-digital converter (ADC). It comes with SPI interface so we can use that read the signal value.

 

It is possible to read the signal value in two modes: MSB and LSB. I used the first one. First you have to read two bytes from the SPI.

  • byte_0 holds two unknown bits, the null bit, and the 5 MSB bits (B11-B07),
  • byte_1 holds the remaining 7 MSB bits (B06-B00) and B01 from the LSB-mode, which has to be removed.

I did some shifting and masking in the get_current_adc function to implement that.

 

int read_ac_current_bytes(uint8_t* byte_1, uint8_t* byte_2)
{
     static const uint8_t sampleCmd = 0x00;
     static const size_t transferCount = 2;
     SPIMaster_Transfer transfers[transferCount];


     int result = SPIMaster_InitTransfers(transfers, transferCount);
     if (result != 0) {
          return -1;
     }

     uint8_t byte1, byte2;

     transfers[0].flags = SPI_TransferFlags_Read;
     transfers[0].readData = &byte1;
     transfers[0].length = sizeof(byte1);


     transfers[1].flags = SPI_TransferFlags_Read;
     transfers[1].readData = &byte2;
     transfers[1].length = sizeof(byte2);


     ssize_t transferredBytes = SPIMaster_TransferSequential(spiFd, transfers, transferCount);

     *byte_1 = byte1;
     *byte_2 = byte2;

     return transferredBytes;
}


float get_current_adc(void)
{
     uint8_t byte_1 = 0, byte_2 = 0;

     read_ac_current_bytes(&byte_1, &byte_2);

     Log_Debug("byte1 = %d, byte2 = %d \n", byte_1, byte_2);


     uint16_t msb_1 = byte_2;
     msb_1 = msb_1 >> 1; // shift right 1 bit to remove B01

     uint16_t msb_0 = byte_1 & 0b00011111; // mask the 2 unknown bits and the null bit
     msb_0 = msb_0 << 7; // shift left 7 bits

     uint16_t msb = msb_0 + msb_1;

     return msb;
}

 

The get_current_adc function result is called Digital Output Code. It is a 12-bit number. It is actually measured probe voltage, so we need to convert that and calculate alternating current.

But first we need to read AC couple of times to get more realistic data.

To get probe voltage (Vin) I used the following equation (from MCP3201 datasheet https://download.mikroe.com/documents/datasheets/mcp3201-datasheet.pdf):

calculation

DigitalOutputCode is what we read from SPI device; Vref is a reference voltage equal to 2.048 V.

Then we need to convert Vin into the AC. I spent so much time trying to calculate that value and I am still not sure if I did that correctly.

 

 

We know that

  • 0-1V output valtage from the probe is equal to 0-30A of AC
  • the signal is amplified by the MCP607 module; the amplification ratio is G = 1 + R4 / R3 = 8.5

 

That gives us equation to calculate AC.

 

   AC=VinG ·30=Vin8.5 ·30

 

Let's use it to calculate alternating current

 

float get_current_ac(uint8_t measurements)
{
     float average = 0.0;
     for (uint8_t i = 0; i < measurements; i++) {
          float adc = get_current_adc();
          Log_Debug("adc = %f \n", adc);
          average += adc;
          HAL_Delay(100);
     }

     average = (float) average / measurements;

     float ac = (float)(average / 4095.0) * 2.048;

     ac = (float)ac / 3.5 * 30.0;

     return ac;
}

 

Microsoft Azure

 

Azure Functions

 

Azure Functions can be used to accomplish a lot of things. They can be triggered in various ways e.g. by timer. I have created three Azure Functions:

 

 

  • IotHub triggered function

It is used to capture messages sent form IoT device (Azure Sphere) to the IoT Hub. As you can see below the purpose of this function is to capture a message that contains calculated alternating current. Then that message is deserialized and added to the table storage on Azure.

 

using IoTHubTrigger = Microsoft.Azure.WebJobs.EventHubTriggerAttribute;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.EventHubs;
using System.Text;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using MeasureBridge.IotHubFunctionApp.InputModel;
using MeasureBridge.Model.Storage;


namespace MeasureBridge.IotHubFunctionApp
{
    public static class IotHubTriggerFunction
    {
        [FunctionName("IotHubTriggerFunction")]
        public static void Run([IoTHubTrigger("messages/events", Connection = "ConnectionString")]EventData message, [Table("ACCurrent")]ICollector<ACCurrent> tableBinding, ILogger log)
        {
            var json = Encoding.UTF8.GetString(message.Body.Array);
            log.LogInformation($"C# IoT Hub trigger function processed a message: {json}");

            var m = JsonConvert.DeserializeObject<Message>(json);

            var entity = new ACCurrent()
            {
                AC = m.ACCurrent,
                PartitionKey = System.Guid.NewGuid().ToString(),
                RowKey = m.ACCurrent.ToString()
            };

            tableBinding.Add(entity);
        }
    }
}

 

  • TimerConsumptionFunctionApp - time triggered

The purpose of this function is to calculate and store the value of used energy (kWh).

 

using System;
using System.Linq;
using System.Threading.Tasks;
using MeasureBridge.Model.Storage;
using MeasureBridge.StorageService.Extensions;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using Microsoft.WindowsAzure.Storage.Table;


namespace MeasureBridge.IotHubFunctionApp
{
    public static class TimerConsumptionFunctionApp
    {
        [FunctionName("TimerConsumptionFunctionApp")]
        [return: Table("Consumption", Connection = "AzureWebJobsStorage")]
        public static async Task<Consumption> Run([TimerTrigger("0 */10 * * * *")]TimerInfo myTimer,
            [Table("ACCurrent")]CloudTable acCurrentTable,
            ILogger log)
        {
            log.LogInformation($"C# TimerConsumptionFunctionApp executed at: {DateTime.Now}");
            
            var currentKwh = await CalculatekWh(acCurrentTable);


            if (currentKwh == 0)
            {
                log.LogInformation($"Calculated kWh is 0");
                return null;
            }


            log.LogInformation($"Saved {currentKwh} to consumption collector");
            var entity = new Consumption()
            {
                kWh = currentKwh,
                PartitionKey = Guid.NewGuid().ToString(),
                RowKey = currentKwh.ToString()
            };


            return entity;
        }


        private async static Task<double> CalculatekWh(CloudTable acCurrentTable)
        {
            int minutes = 10;
            DateTime from = DateTime.Now.AddMinutes(-minutes);


            var filter = TableQuery.CombineFilters(TableQuery.GenerateFilterConditionForDate("Timestamp", QueryComparisons.GreaterThanOrEqual, from),
                TableOperators.And,
                TableQuery.GenerateFilterConditionForDate("Timestamp", QueryComparisons.LessThanOrEqual, DateTime.Now));
            TableQuery<ACCurrent> query = new TableQuery<ACCurrent>().Where(filter);


            var result = await acCurrentTable.ExecuteQueryAsync(query);


            var averageAC = result.Any() ? result.Select(s => s.AC).Sum() / (12 * minutes) : 0;


            double kpower = (averageAC * 230) / 1000;
            
            double kWh = kpower * ((double)minutes / 60);


            return kWh;
        }
    }
}

 

  • TimerReportFunctionApp - time triggered

The purpose of this function is to get average of alternating current for the past hour; and also the usages of energy - kWh for the past past, 7 days and a month. 

 

using System;
using System.Linq;
using System.Threading.Tasks;
using MeasureBridge.Model.Storage;
using MeasureBridge.StorageService.Extensions;
using Microsoft.Azure.Devices;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.WindowsAzure.Storage.Table;


namespace MeasureBridge.IotHubFunctionApp
{
    public static class TimerReportFunctionApp
    {
        [FunctionName("TimerReportFunctionApp")]
        public static async void Run([TimerTrigger("0 */5 * * * *")]TimerInfo myTimer, [Table("ACCurrent")]CloudTable testTable, [Table("Consumption")]CloudTable consumptionTable,
            ILogger log, ExecutionContext context)
        {
            log.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");


            var average = await GetAverageAc(testTable);
            (double kWhToday, double kWhLast7Days, double kWhLastMonth) = await GetkWhStats(consumptionTable);


            var config = new ConfigurationBuilder()
             .SetBasePath(context.FunctionAppDirectory)
             .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
             .AddEnvironmentVariables()
             .Build();


            using (var registryManager = RegistryManager.CreateFromConnectionString(config["DeviceConnectionString"]))
            {
                var deviceID = config["DeviceId"];
                var twin = await registryManager.GetTwinAsync(deviceID);
                twin.Properties.Desired["acAverageLastHour"] = average;
                twin.Properties.Desired["kWhToday"] = kWhToday;
                twin.Properties.Desired["kWhLast7Days"] = kWhLast7Days;
                twin.Properties.Desired["kWhLastMonth"] = kWhLastMonth;
                await registryManager.UpdateTwinAsync(deviceID, twin, twin.ETag);
            }
        }


        private async static Task<double> GetAverageAc(CloudTable acCurrentTable)
        {
            DateTime from = DateTime.Now.AddHours(-1);


            var filter = TableQuery.CombineFilters(TableQuery.GenerateFilterConditionForDate("Timestamp", QueryComparisons.GreaterThanOrEqual, from),
                TableOperators.And,
                TableQuery.GenerateFilterConditionForDate("Timestamp", QueryComparisons.LessThanOrEqual, DateTime.Now));
            TableQuery<ACCurrent> query = new TableQuery<ACCurrent>().Where(filter);


            var result = await acCurrentTable.ExecuteQueryAsync(query);


            var average = result.Any() ? result.Select(s => s.AC).Sum() / (12 * 60) : 0;
            
            return average;
        }


        private async static Task<Tuple<double, double, double>> GetkWhStats(CloudTable consumptionTable)
        {
            DateTime from = DateTime.Now.AddMonths(-1);


            var filter = TableQuery.CombineFilters(TableQuery.GenerateFilterConditionForDate("Timestamp", QueryComparisons.GreaterThanOrEqual, from),
                TableOperators.And,
                TableQuery.GenerateFilterConditionForDate("Timestamp", QueryComparisons.LessThanOrEqual, DateTime.Now));
            TableQuery<Consumption> query = new TableQuery<Consumption>().Where(filter);


            var result = await consumptionTable.ExecuteQueryAsync(query);


            var today = result.Where(r => r.Timestamp >= DateTime.Today).Select(s => s.kWh).DefaultIfEmpty().Sum();
            var last7Days = result.Where(r => r.Timestamp >= DateTime.Today.AddDays(-7)).Select(s => s.kWh).DefaultIfEmpty().Sum();
            var lastMonth = result.Select(s => s.kWh).DefaultIfEmpty().Sum();
            
            return Tuple.Create(today, last7Days, lastMonth);
        }
    }
}

 

 

HomeBridge

 

I have couple of HomeKit accessories in my apartment. As you probably know, it is a way of making you home smart. However, some devices are not compatible with Apple HomeKit. But there is also a solution for that - HomeBidge. HomeBridge is a lightweight NodeJS server that emulates the iOS HomeKit API. It supports plugins - modules that provide a basic bridge from HomeKit to various devices. I have HomeBridge server installed on the RaspberryPi 3b.

 

One of the project goals was to be able to turn on and off the electric device connect to the MeasureBridge. The device is turned on/off by MikroE Relay click board. I used device twin properties to control state of the relay. Microsoft provides SDK for NodeJs, so I decided to create a homebridge plugin. The plugin updates the desired of twin properties on the Azure IoT Hub. Then hub sends twin properties to the AzureSphere device. At the end, the device is opening or closing the relay.

 

 

homebridge schema

 

Here is the code for the HomeBridge plugin

var Registry = require('azure-iothub').Registry;
var Service, Characteristic;


module.exports = function (homebridge) {
    Service = homebridge.hap.Service;
    Characteristic = homebridge.hap.Characteristic;


    homebridge.registerAccessory("homebridge-azure-iot-hub-switch", "AzureSwitch", AzureSwitch);
};


function AzureSwitch(log, config) {
    var self = this;
    self.log = log;
    self.config = config;
    self.name = config["name"];
    self.connectionString = config["connectionString"];
    self.targetDevice = config["targetDevice"];
    self.deviceTwinName = config["deviceTwinName"];
    self.state = false;


    self.registry = Registry.fromConnectionString(self.connectionString);


    this.service = new Service.Switch(this.name);
    this.service
        .getCharacteristic(Characteristic.On)
        .on('get', this.getOn.bind(this))
        .on('set', this.setOn.bind(this));
}


AzureSwitch.prototype.getOn = function (callback) {
    callback(null, this.state);
};


AzureSwitch.prototype.setOn = function (on, callback) {
    this.state = on ? true : false;
    var self = this;
    var desired = {};
    desired[self.deviceTwinName] = self.state;


    self.registry.getTwin(self.targetDevice, function (err, twin) {
        if (err) {
            console.error(err.message);
        } else {
            console.log(JSON.stringify(twin, null, 2));
            self.twinEtag = twin.etag;


            self.registry.updateTwin(self.targetDevice, { properties: { desired: desired } }, self.twinEtag, function (err, twin) {
                if (err) {
                    console.error(err.message);
                } else {
                    console.log(JSON.stringify(twin, null, 2));
                    self.twinEtag = twin.etag;
                }
            });
        }
    });


    callback(null, on);
};


AzureSwitch.prototype.getServices = function () {
    return [this.service];
};

 

However, it is also available as a npm plugin https://www.npmjs.com/package/homebridge-azure-iot-hub-switch . You can install it by typing

 

npm install -g homebridge-azure-iot-hub-switch

 

You have to specify couple of things first in the HomeBtidge config.json file

  • name - name of the device to display in the Home app
  • connectionString - connection string to the Azure IoT Hub Shared Access service

azure iot hub connection string

  • targetDevice - name of you Azure IoT Device
  • deviceTwinName - name of the twin property

Here is an example:

  "accessories": [
    {
       "accessory": "AzureSwitch",
       "name" : "Azure Switch 1",
       "connectionString" : "HostName=xxx.azure-devices.net;SharedAccessKeyName=service;SharedAccessKey=xxx",
       "targetDevice" : "MeasureBridgeDev",
       "deviceTwinName" : "clickBoardRelay1"
    }
  ]

 

and the test video

 

 

Let's put it all together

 

{gallery} Build

all in one box

all in one box

all in one box

 

Now let's connect an electric device and test it.

 

 

 

 

Summary

 

So I had a lot of fun building that device and cool that it worked at the end It was very challenging for me, specially a part getting value of alternating current from MikroE AC Current Click board.

I also want to thank Avnet team for demo projects that helped me getting started.

 

What's next?

 

For sure there is a place for improvements. I don't want to stop so I will continue developing that project.

  • Improve OLED display; add icons, make it more user friendly
  • Turn off OLED when the device is not used and relay state is off
  • It would be nice to have couple of 3d printed parts to attach devices