In my last post, I showed how we can connect individual room controllers to the main hub( raspberry pi ) using a home network and also how Paho MQTT library comes handy for the communication between them. Basically a room controller takes care of all the controlling functions of devices in a room. Each one runs linux and have a python script using Paho MQTT library for networking and GPIO libraries for controlling the devices. Now my system have the added advantage that openHAB doesn't have to directly control the hardwares, rather send MQTT messages and each room controller takes care of the rest. In my rPi hosting openHAB, I have a similar python script to take care of the devices attached to it.
From the very first of this project, I was thinking of a solution to control all the devices in my home with a single device. When I'm in main hall of the house, this device should adapt to control the devices in that main hall. As I move to bedroom, the same device should adapt to the devices in bedroom. So the first option was to use a smartphone connected to openHAB. In that case, the story will go like this. When I'm in main hall, load the page for main hall from openhab, do what ever I want. When I go to bedroom, go back to openHAB main page, load the page for bedroom and continue using it as long as I'm in bedroom. But here is the problem, I don't want to load each page every time. When I'm main hall, it should detect that I'm in main hall and control the devices accordingly. And as I move to bedroom, detect that I'm in bedroom and adapt the controls for bedroom. And same with my kitchen too. So loading a page in smart phone not at all impressed me. Besides the phone need to be connected to WiFi all the time for this. And as you know, WiFi likes battery very much and it drains the power as fast as it can. So clearly phone is not my option.
Then I thought I can use a remote control. Yup! go to main hall, point the remote control to your light, press '1' and it turns ON. Now go to bedroom, point it your your bedlamp, press '2' and it turns ON. Seems kind of perfect. But again who remembers all these numbers - '1' for main hall, '2' for bedroom....!!! So now use the same button to control the devices in each room. But IR rays are more disobedient than young kids, they occasionally leaks to next room and switches the devices there. Besides RCs are more bulky. So I ruled out that option also. I want some simple device with just "One Switch" which is compact enough to carry around and works like magic with each room. Then one day( night actually) I was playing with rocker switch and Enocean Protocol while developing Enocean-Py, I came across an interesting byte in ESP radio packet which gave me the clue to do this. Perfect!!
In this post, I'm going to show how you can use same rocker switch to control devices in multiple rooms. It will work like this. You go to your main hall with your rocker switch, push 'ON' and the lamp in main hall turns on. Now you go to your kitchen with same rocker switch, CLICK!! and your kitchen lamp turns ON. Now go to bedroom, CLICK the same rocker and your bedroom lamp turns ON!! Yes, you can use same rocker switch to control them all. As you would have guessed, I'm using some kind of localization technique to locate my rocker switch inside the house. In theory, you need three receivers to locate a wireless signal sender. But what we have is only our enocean gateway pi receiver, right? Thanks to the Enocean Sensor Protocol, MQTT, my design of room controllers and the Enocean Programmer which came as a surprise gift, I'm able to achieve it with no more components.
So the first piece of the answer lies in the Enocean Serial Protocol. If you look at an ESP packet for a radio from rocker switch, it has a header part 5+1 Bytes, Data part of 7Bytes, Optional Data part of 7 Bytes and a CRC of 1 byte length at the end. It will look like this :
Here you can see that 6th byte in optional data is marked as 'dBm'. It is a relative measure of the strength of the signal received at the gateway receiver from the sensors, in this case a rocker switch. It always has a negative value and we call it RSSI. So if you read -40dBm for radio message by clicking rocker near the gateway, you may read a -60dBm if you are away from gateway. So this measure can tell you how near you are to the gateway while clicking the rocker. Now if I have multiple gateways placed in each room of my home, they all will( hopefully ) detect a rocker switch click and based on the RSSI, you will be able to detect in which room the rocker switch is clicked. So the first piece of the puzzle is solved.
Second piece is a little more interesting. We a provided with only one Enocean-PI gateways. Now from where will I get a second gateway? Then came the savior EOP350!! When I first got EOP on courier, I didn't even know what is it's purpose. Then I learned it's the programmer for Enocean devcies. Anyway I am not messing with those sensor firmwares, so it remained in my shelf unopened for a few weeks. It turned out that the TCM320C included in the EOP350 comes preloaded with a sniffer software that will allow windows users to monitor their network activity. The sniffer protocol it a little bit complex that the ESP so I was not able to play with it much. Then I stumbled across the software downloads section in Enocean website and I found the firmware for Gateway control for TCM320C. Perfect!! I burned this new firmware to TCM320C using EOP and yayy...I have two gateways now.
Third piece is even more interesting. Now I have two enocean gateways, one attached to raspberry pi and other to BBB, placed in different rooms, how to coordinate their activity and select the right controller to switch the device. This needs a strong and reliable network connecting them together and a program to do some kind of centralized leader election every moment. Nothing comes handy than our Eclispse Paho project for this. As three of my four room controllers are running linux, I can write a single code and deploy it on all the three nodes with great easiness. So this is how my home network looks now:
I have a raspberry pi at main hall, beagle bone black at kitchen, intel galileo at bedroom and TI connected launchpad for controlling the lights outside house. Every node except the last one is running a derivative of debian linux. Enocean Pi attached to rPi and EOP350 with Gateway control TCM320C to BBB. I have Mosquitto server running at BBB and all the nodes are connected to it. Now when I click rocker switch standing in mainhall, both rPi and BBB will detect it. I have similar python scripts running on both which will take care of packaging relevant information from the gateaway controller to a MQTT packet and publish it to network, ofcourse in different topic names. Then I have this 'oneSwitch_master.py' running at BBB which is subscribed to those topics published by the room controllers. When it gets first packet from a room controller, it enters a mode of polling for next 500ms for simillar message from any other room controller. If it gets any thing new, based on the signal strength in the packet, this script will select which room controller won the election. Otherwise, the only one who send it the message wins the election. Then this master establishes a corresponding message to the room controller to switch on the device. For now, I have configured it to switch ON lamp0 in that room. But this is easily re configurable. Here, the master script is just emulating the openHAB and issues the control messages on behalf of it. All this background processing happens at lighting fast speed that the user didn't even notice the delay.
Technical details
I used Eclipse Paho Python library and Enocean-Py at each node to take radio packets from air, process it and put it on network. Complete script is given below:
Script at raspberry pi:
# one_switch.py -- Using enocean rocker switch as universal remote # # Copyright 2014 Vishnu Raj <rajvishnu90@gmail.com> # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # # import paho.mqtt.client as mqtt import paho.mqtt.publish as publish import time import EO import ESP ttyPort = "/dev/ttyAMA0" #~ ttyPort = "/dev/ttyUSB1" rockerId = [ 0x00, 0x1A, 0x34, 0x82 ] # MQTT network definitions basePath = 'homenet/area' nodeArea = 'mainHall' def onConnect( client, userData, retCode ): print( "Connected with return code " + str( retCode ) ); # publish a entry message client.publish( "homenet/devices/oneSwitch/" + str(nodeArea), '{"role":"oneSwitch","area":"' + nodeArea + '"}' ); ''' Function : main Description : main function Arguments : none Returns : none ''' def main(): print '\t\t One Switch to control them all\n' print "\tUsing serial port : " + ttyPort + '\n' # CO_RD_IDBASE command cmd1 = [ 0x55, 0x00, 0x01, 0x00, 0x05, 0x70, 0x08, 0x38 ] hEOGateway = EO.connect( ttyPort ) # better to wait a little for connection to establish time.sleep( 0.100 ) ## clear buffer of any recieved data EO.receiveData( hEOGateway ) # basic check whether an enocean gateway device is attached # send command and check for RESPONSE # CO_RD_IDBASE command cmd = [ 0x55, 0x00, 0x01, 0x00, 0x05, 0x70, 0x08, 0x38 ] rawResp = EO.sendData( hEOGateway, cmd ) pkt = ESP.decodePacket( rawResp ); # check whether we got a valid reponse if( pkt['crc8h_stat'] == 'OK' and pkt['pktType'] == ESP.ESP_packetTypes['RESPONSE'] ): print 'Enocean Gateway send me a Hi :)' else: print 'STAT : ' + str( pkt['crc8h_stat'] ) print 'RESP : ' + str( pkt['pktType'] ); print 'Seems like you misplaced the connections. Call your Moma!!!' # connect to MQTT broker client = mqtt.Client( client_id = "oneSwitch_" + nodeArea, clean_session = True, userdata = None, protocol = mqtt.MQTTv31 ) # add callbacks client.on_connect = onConnect # Connect to broker client.connect( "192.168.1.79", 1883, 60 ); # start a thread for connection client.loop_start(); # now listen to radios and forward to centre try: while( True ): rawResp = EO.receiveData( hEOGateway ) if rawResp: # we recived a radio pkts = ESP.decodeRawResponse( rawResp ) for pkt in pkts: #~ print "PACKET : " #~ print ' Data : ', ' '.join(map(hex,pkt['data_recv'])) #~ print ' Optinal Data : ', ' '.join(map(hex,pkt['opData_recv'])) #~ print ' Strength : ' + str( -1*pkt['opData_recv'][5]) # check whether this packet is from a rocker switch if( pkt['data_recv'][0] == 0xF6 ): # check the ID of rocker switch if( pkt['data_recv'][2] == rockerId[0] and pkt['data_recv'][3] == rockerId[1] and pkt['data_recv'][4] == rockerId[2] and pkt['data_recv'][5] == rockerId[3] ): # prepare messagae msg = '{"area":"' + nodeArea + '",' msg += '"strength":"'+ str( -1*pkt['opData_recv'][5] ) + '","command":'; if( pkt['data_recv'][1] == 0x50 ): msg += '"ON"' elif( pkt['data_recv'][1] == 0x70 ): msg += '"OFF"' elif( pkt['data_recv'][1] == 0x00 ): msg += '"RELEASE"' else: msg += '"UNKNOWN"' msg += '}' client.publish( basePath + '/' + nodeArea + '/' + 'oneSwitch', msg ); else: # recieved Id didn't match # print "Not call to me :(" pass else: # not a rocker #~ print "Who's making noise there?" pass except KeyboardInterrupt: print "\nExiting one_switch" EO.disconnect( hEOGateway ) print "Bye..bye.. :) " if __name__ == "__main__": main()
Script at BBB:
# one_switch.py -- Using enocean rocker switch as universal remote # # Copyright 2014 Vishnu Raj <rajvishnu90@gmail.com> # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # # import paho.mqtt.client as mqtt import paho.mqtt.publish as publish import time import EO import ESP #~ ttyPort = "/dev/ttyAMA0" ttyPort = "/dev/ttyUSB1" rockerId = [ 0x00, 0x1A, 0x34, 0x82 ] # MQTT network definitions basePath = 'homenet/area' nodeArea = 'kitchen' def onConnect( client, userData, retCode ): print( "Connected with return code " + str( retCode ) ); # publish a entry message client.publish( "homenet/devices/oneSwitch/" + str(nodeArea), '{"role":"oneSwitch","area":"' + nodeArea + '"}' ); ''' Function : main Description : main function Arguments : none Returns : none ''' def main(): print '\t\t One Switch to control them all\n' print "\tUsing serial port : " + ttyPort + '\n' # CO_RD_IDBASE command cmd1 = [ 0x55, 0x00, 0x01, 0x00, 0x05, 0x70, 0x08, 0x38 ] hEOGateway = EO.connect( ttyPort ) # better to wait a little for connection to establish time.sleep( 0.100 ) ## clear buffer of any recieved data EO.receiveData( hEOGateway ) # basic check whether an enocean gateway device is attached # send command and check for RESPONSE # CO_RD_IDBASE command cmd = [ 0x55, 0x00, 0x01, 0x00, 0x05, 0x70, 0x08, 0x38 ] rawResp = EO.sendData( hEOGateway, cmd ) pkt = ESP.decodePacket( rawResp ); # check whether we got a valid reponse if( pkt['crc8h_stat'] == 'OK' and pkt['pktType'] == ESP.ESP_packetTypes['RESPONSE'] ): print 'Enocean Gateway send me a Hi :)' else: print 'STAT : ' + str( pkt['crc8h_stat'] ) print 'RESP : ' + str( pkt['pktType'] ); print 'Seems like you misplaced the connections. Call your Moma!!!' # connect to MQTT broker client = mqtt.Client( client_id = "oneSwitch_" + nodeArea, clean_session = True, userdata = None, protocol = mqtt.MQTTv31 ) # add callbacks client.on_connect = onConnect # Connect to broker client.connect( "192.168.1.79", 1883, 60 ); # start a thread for connection client.loop_start(); # now listen to radios and forward to centre try: while( True ): rawResp = EO.receiveData( hEOGateway ) if rawResp: # we recived a radio pkts = ESP.decodeRawResponse( rawResp ) for pkt in pkts: #~ print "PACKET : " #~ print ' Data : ', ' '.join(map(hex,pkt['data_recv'])) #~ print ' Optinal Data : ', ' '.join(map(hex,pkt['opData_recv'])) #~ print ' Strength : ' + str( -1*pkt['opData_recv'][5]) # check whether this packet is from a rocker switch if( pkt['data_recv'][0] == 0xF6 ): # check the ID of rocker switch if( pkt['data_recv'][2] == rockerId[0] and pkt['data_recv'][3] == rockerId[1] and pkt['data_recv'][4] == rockerId[2] and pkt['data_recv'][5] == rockerId[3] ): # prepare messagae msg = '{"area":"' + nodeArea + '",' msg += '"strength":"'+ str( -1*pkt['opData_recv'][5] ) + '","command":'; if( pkt['data_recv'][1] == 0x50 ): msg += '"ON"' elif( pkt['data_recv'][1] == 0x70 ): msg += '"OFF"' elif( pkt['data_recv'][1] == 0x00 ): msg += '"RELEASE"' else: msg += '"UNKNOWN"' msg += '}' client.publish( basePath + '/' + nodeArea + '/' + 'oneSwitch', msg ); else: # recieved Id didn't match # print "Not call to me :(" pass else: # not a rocker #~ print "Who's making noise there?" pass except KeyboardInterrupt: print "\nExiting one_switch" EO.disconnect( hEOGateway ) print "Bye..bye.. :) " if __name__ == "__main__": main()
Below is the master python script which does the centralized leader election. This can be run at any node in the network, I'm running this at my BBB :
# one_master : Use enocean rocker switch as universal remote # # Copyright 2014 Vishnu Raj <rajvishnu90@gmail.com> # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # # import paho.mqtt.client as mqtt import paho.mqtt.publish as publish import re import time from datetime import datetime from datetime import timedelta start_time = datetime.now() # MQTT network definitions basePath = 'homenet/area' nodeArea = 'master' # for publishing message selArea = "Unknown" selCommand = "UNKONWN" bufferFlag = 0 # if '0' then nothing to send to openhab lstMsgTime = datetime.now() selStrength = -130 # some very low power signal # returns the elapsed milliseconds since the start of the program def millis(): dt = datetime.now() - start_time ms = (dt.days * 24 * 60 * 60 + dt.seconds) * 1000 + dt.microseconds / 1000.0 return ms def onConnect( client, userData, retCode ): print( " Connected with return code " + str( retCode ) ); # publish a entry message client.publish( "homenet/devices/oneSwitch/master", '{"role":"oneMaster"' ); def onMessage( client, userData, msg ): global selArea, selCommand, bufferFlag, lstMsgTime, selStrength #~ print( str(millis()) + " " + msg.topic + " " + str( msg.payload) ) # extract data from this message msgFields = re.match( r'{"area":"(.*)","strength":"(.*)","command":"(.*)"}', str(msg.payload), re.M|re.I); area = msgFields.group(1) strength = int(msgFields.group(2)) command = msgFields.group(3) # if buffer is empty if( bufferFlag == 0 ): # then this is the first message in this shot selArea = area selCommand = command selStrength = strength bufferFlag = 1 else: # we already have atleast one messgae selected to forward to openHAB if( selStrength < strength ): selArea = area selCommand = command selStrength = strength lstMsgTime = millis() def main(): global selArea, selCommand, bufferFlag, lstMsgTime, selStrength print '\t\t One Switch to control them all\n' # connect to MQTT broker client = mqtt.Client( client_id = "oneSwitch_" + nodeArea, clean_session = True, userdata = None, protocol = mqtt.MQTTv31 ) # add callbacks client.on_connect = onConnect client.on_message = onMessage # Connect to broker client.connect( "192.168.1.79", 1883, 60 ); # start a thread for connection client.loop_start(); # subscribe to oneswitch messages client.subscribe( basePath + '/+/oneSwitch' ); try: while( True ): if( bufferFlag != 0 ): # we have something to send if( millis() - lstMsgTime > 500 ): # we didn't recieved any message for last 500ms pubTopic = basePath + '/' + selArea + '/lamp0' pubMsg = str( selCommand ) print "Send Message " + pubMsg + " on " + pubTopic client.publish( pubTopic, pubMsg ) bufferFlag = 0; except KeyboardInterrupt: print "Bye..bye.. :) " if __name__ == "__main__": main()
You need to have Enocean-Py library to run the "one_swicth.py" scripts. For more,read this [FMN#03] : Decoding Enocean Protocol with Enocean Py
That's it!!! With this experiment, I really leaned how great MQTT is for these kind of stuff and how handy Paho project is while developing. Thanks for all the developers who made it possible.
In my next post, I'll be wiring everything together and presenting the whole stuff
Happy hacking,
vish