Quantcast
Channel: Desert Home
Viewing all articles
Browse latest Browse all 218

Hacking Into The Iris Door Sensor, Part 3, The code

$
0
0
The previous post in the project is here <link>

Last time I described the Key Fob that Lowe's sells and previous to that I showed the Door Switch, but I haven't shown you the way I got them to work. I'm still a little reluctant because my partner in this project hasn't succeeded in getting his devices to work properly, but it occurred to me that someone out there might pick up the code and give it a try. I know several folk have used my Smart Switch code to control and monitor things around the house, maybe they're just waiting for me to publish how to do it.

At any rate, this is how I got the switches and key fob to behave:

I have eight devices, five of the smart switches, two door switches, and a key fob, and all of them are working with the same monitoring code. The code is only to monitor the devices, there's no provisions for control, Actually, the only thing you can control is the smart switches, and I've already posted code for that, so this will show you how to monitor all the devices. This code works pretty easily, pull the battery out of the device, start the code, put the battery back in and push the button eight times quickly. The switch will contact the code and join on its own. Once joined, the door switch and key fob will send status every two minutes telling you they're alive and in range. The smart switches send status much more often with a cumulative status every minute. If you want to join other devices, just pull the battery, put it back, and push the button a bunch of times; joining is always enabled and the next device should work fine. Stopping the code (cntrl-C) and restarting it won't cause the devices to leave the network, they'll be fine when you restart the code. I did this a LOT when I was trying to get them to work.

I could go into intimate details of how the interaction works, but the code will show you this much better. What happens is that the switch sends an device announce message and the code will respond and then they exchange data. During this there are two specific message that are sent to Alertme devices to get them to accept the XBee coordinator; this is the secret part of the Iris system that makes these devices unique. The code I came up with for the smart switches wouldn't work because the door switch and key fob are slower devices and I was hitting them too quickly with the initialization messages. They didn't have time to recognize and react to them. A couple of sleep calls took care of that problem. I used the same setup on the XBee as when I hacked into the Smart Switch, you can look at it here <link>

Once running, you can press a switch on the key fob and it will transmit a messsage telling you that the button was pressed or released along with a counter that represents the time in milliseconds off the action. By subtracting the released time from the pressed time you can look for a long press as opposed to a short press. Nice feature.

The door switch has two switches, a tamper switch that will tell you if the cover is taken off and a reed switch that tells you the door has been opened. The tamper switch will tell you press and release like the key fob does, as will the reed switch. This means you can tell if the door is opened or closed as well as the cover off or on.

Like the smart switches, the key fob and door switch once joined, stay that way. You can pull the battery and put it back in without it having a problem. You can also drive away out of range and come back and everything works just fine. If you kill the coordinator by pulling the power or something, you might have to make them rejoin. This all depends on how long it was off and what happened during the off period. Sometimes the coordinator XBee will choose a different channel and then the switches will have trouble finding it. A simple power failure won't cause any problem, but reloading software to the controller could mean a visit to the devices to force them to rejoin. This kind of problem can be avoided by using the parameters on the XBee controller to limit it to a single predetermined channel. Doing this means the XBee comes up, establishes a network on the channel and the devices then just continue on as before.

So, you're going to get a lot of data as you add devices. Each device will interact at least every couple of minutes and every time you mess with them. For me, this meant missed messages. The traffic was pretty steady, but things didn't look right until I added a queue to the code to hold the messages and took them off one at a time to deal with. There's plenty of unused time to handle the messages, but they tend to come in in bunches and can cause problems. Adding a queue seemed to help that problem a lot; I don't see any missing or partial messages when I run it.

There's lots of debug and logging in the code as well as a test to help you isolate a single device. You'll see it in the code; I look for a list of devices to listen to and ignore the others. I had to do this to keep the other devices on the network from confusing me when I was trying figure out which bit meant what. Look for the test at the top of the code to decode the messages and adjust it as you need to. Also, this doesn't save anything from one run to the next. There's no database of devices that gets updated and the state of the devices isn't saved to compare with later. Once you're comfortable with the code, simply add whatever you want to handle things. I'm going to hook this code in with my database of house devices to save the states and decide exactly what I want to do with it later when I have a little more experience.

Here's the code:

#! /usr/bin/python

'''
This is a hack into the operation of the Iris Smart Home Smart
Plug, Door Switch, and Key Fob. The evolution of this is discussed
on Desert-Home.com in detail.

Have fun
'''

from apscheduler.schedulers.background import BackgroundScheduler
from xbee import ZigBee
import logging
import datetime
import Queue
import time
import serial
import sys, traceback
import shlex
from struct import *

def printData(data):
print "********** Message Contents"
for key, value in data.iteritems():
if key == "id":
print key, value
else:
print key, "".join("%02x " % ord(b) for b in data[key])
print "**********"

def clusterData(lAddr,clusterId, data):
print int(time.time()),
print "".join("%02x" % ord(b) for b in lAddr) + \
" clid "+"%04x" % clusterId + "-" + \
"".join("%02x " % ord(b) for b in data)

# this is a call back function. When a message
# comes in this function will get the data
# I had to use a queue to make sure there was enough time to
# decode the incoming messages. Otherwise, in heavy traffic
# periods, I'd get a new message while I was still working on
# the last one.
def messageReceived(data):
#print "queueing message"
messageQueue.put(data)

def handleMessage (data):
try:
'''
I have a network of these devices and had to
add this test to keep the log down to a reasonable
size. If all the devices show up, it's hard to
tell what is going on. Just uncomment the check
and put whatever you want to watch in.
'''

'''if data['source_addr_long'] not in \
['\x00\x0d\x6f\x00\x03\xc2\x71\xcc',
'\x00\x0d\x6f\x00\x02\x83\xfa\x4e']:
return'''

#print ''
#print 'gotta packet',
#printData(data)
if (data['id'] == 'rx_explicit'):
#print "RX Explicit"
#printData(data)
clusterId = (ord(data['cluster'][0])*256) + ord(data['cluster'][1])
#print 'Cluster ID:', hex(clusterId),
#print "profile id:", repr(data['profile'])

if (data['profile']=='\x00\x00'): # The General Profile
print 'Cluster ID:', hex(clusterId),
print "profile id:", repr(data['profile'])
if (clusterId == 0x0000):
print ("Network (16-bit) Address Request")
#printData(data)
elif (clusterId == 0x0004):
# Simple Descriptor Request,
print("Simple Descriptor Request")
#printData(data)
elif (clusterId == 0x0005):
# Active Endpoint Request,
print("Active Endpoint Request")
#printData(data)
elif (clusterId == 0x0006):
print "Match Descriptor Request"
#printData(data)
time.sleep(2)
print "Sending match descriptor response"
zb.send('tx_explicit',
dest_addr_long = data['source_addr_long'],
dest_addr = data['source_addr'],
src_endpoint = '\x00',
dest_endpoint = '\x00',
cluster = '\x80\x06',
profile = '\x00\x00',
options = '\x01',
data = '\x04\x00\x00\x00\x01\x02'
)
# The contact switch is a bit slow, give it
# some time to digest the messages.
time.sleep(2)
zb.send('tx_explicit',
dest_addr_long = data['source_addr_long'],
dest_addr = data['source_addr'],
src_endpoint = '\x02',
dest_endpoint = '\x02',
cluster = '\x00\xf6',
profile = '\xc2\x16',
data = '\x11\x01\xfc'
)
time.sleep(2)
elif (clusterId == 0x0008):
# I couldn't find a definition for this
print("This was probably sent to the wrong profile")
elif (clusterId == 0x0013):
# This is the device announce message.
print 'Device Announce Message'
# this will tell me the address of the new thing
# so I'm going to send an active endpoint request
print 'Sending active endpoint request'
epc = '\xaa'+data['source_addr'][1]+data['source_addr'][0]
print "".join("%02x " % ord(b) for b in epc)
zb.send('tx_explicit',
dest_addr_long = data['source_addr_long'],
dest_addr = data['source_addr'],
src_endpoint = '\x00',
dest_endpoint = '\x00',
cluster = '\x00\x05',
profile = '\x00\x00',
options = '\x01',
data = epc
)

#printData(data)
elif (clusterId == 0x8000):
print("Network (16-bit) Address Response")
#printData(data)
elif (clusterId == 0x8038):
print("Management Network Update Request");
elif (clusterId == 0x8005):
# this is the Active Endpoint Response This message tells you
# what the device can do
print 'Active Endpoint Response'
printData(data)
elif (clusterId == 0x8004):
print "simple descriptor response"
else:
print ("Unimplemented Cluster ID", hex(clusterId))
print
elif (data['profile']=='\xc2\x16'): # Alertme Specific
if data['source_addr_long'] not in devices:
devices.setdefault(data['source_addr_long'], []).append(data['source_addr'])
# suppress printing for known clusters
# so I can look at it more closely
if clusterId not in [0X0500, 0x00ef, 0x00f0, 0x00f2, 0x00f3,0x00f6]:
printData(data)
print "Unhandled Message"
if (clusterId == 0xef):
clusterData(data['source_addr_long'],clusterId,data['rf_data'])
clusterCmd = ord(data['rf_data'][2])
status = data['rf_data'] # cut down on typing
if (clusterCmd == 0x81):
usage = unpack('<H', status[3:5])[0]
print " Current Usage:", usage
elif (clusterCmd == 0x82):
usage = unpack('<L', status[3:7])[0] / 3600
upTime = unpack('<L', status[7:11])[0]
print (" Switch Minute Stats: Usage, %d Watt Hours; Uptime, %d Seconds" %(usage/3600, upTime))
elif (clusterId == 0x00f6):
clusterData(data['source_addr_long'],clusterId,data['rf_data'])
print ''
print "Identify Message"
print "Sending init message"
zb.send('tx_explicit',
dest_addr_long = data['source_addr_long'],
dest_addr = data['source_addr'],
src_endpoint = '\x00',
dest_endpoint = '\x02',
cluster = '\x00\xf0',
profile = '\xc2\x16',
data = '\x19\x41\xfa\x00\x01'
)
elif (clusterId == 0x00f3):
clusterData(data['source_addr_long'],clusterId,data['rf_data'])
print ' Key Fob Button',
status = data['rf_data']
print ord(status[3]),
if status[2] == '\x01':
print 'Closed',
elif status[2] == '\x00':
print 'Open',
else:
print 'Unknown',
print 'Counter', unpack('<H',status[5:7])[0],
print ''
pass
elif (clusterId == 0x00f2):
clusterData(data['source_addr_long'],clusterId,data['rf_data'])
print 'Tamper Switch Changed State'
pass
elif (clusterId == 0x00f0):
clusterData(data['source_addr_long'],clusterId,data['rf_data'])
# If the cluster cmd byte is 'xfb', it's a status
if data['rf_data'][2] == '\xfb':
status = data['rf_data'] # just to make typing easier
if status[3] == '\x1f':
print " Door Sensor",
print str(float(unpack("<h", status[8:10])[0])\
/ 100.0 * 1.8 + 32) + "F",
elif status[3] == '\x1c':
# Never found anything useful in this
print "Power Switch",
elif status[3] == '\x1d':
print " Key Fob",
print str(float(unpack("<h", status[8:10])[0])\
/ 100.0 * 1.8 + 32) + "F",
unpack('<I',status[4:8])[0]
print 'Counter', unpack('<I',status[4:8])[0],
elif status[3] == '\x1e':
# I haven't figured out what this is yet
# it comes from a door switch and the temperature
# field is always ff ff, it may be an error
# indication.
pass
else:
print " Don't know this device yet",
print ''
pass
elif (clusterId == 0x0500): # This is the security cluster
clusterData(data['source_addr_long'],clusterId,data['rf_data'])
# When the switch first connects, it come up in a state that needs
# initialization, this command seems to take care of that.
# So, look at the value of the data and send the command.
if data['rf_data'][3:7] == '\x15\x00\x39\x10':
print "sending initialization"
zb.send('tx_explicit',
dest_addr_long = data['source_addr_long'],
dest_addr = data['source_addr'],
src_endpoint = '\x00',
dest_endpoint = '\x00',
cluster = '\x05\x00',
profile = '\xc2\x16',
data = '\x11\x80\x00\x00\x05'
)
# The switch state is in byte [3] and is a bitfield
# bit 0 is the magnetic reed switch state
# bit 3 is the tamper switch state
switchState = ord(data['rf_data'][3])
if switchState & 0x04:
print 'Tamper Switch Closed',
else:
print 'Tamper Switch Open',
if switchState & 0x01:
print 'Reed Switch Opened',
else:
print 'Reed Switch Closed',
print ''
pass
else:
print ("Unimplemented Profile ID")
elif(data['id'] == 'route_record_indicator'):
print("Route Record Indicator")
else:
print("some other type of packet")
print(data)
except:
print "I didn't expect this error:", sys.exc_info()[0]
traceback.print_exc()

def showDevices():
print "Known Devices ************"
for key in devices:
print "".join("%02x " % ord(b) for b in key)+':',
print "".join("%02x " % ord(b) for b in devices[key][0])
print "**************************"

def sendSwitch(whereLong, whereShort, srcEndpoint, destEndpoint,
clusterId, profileId, clusterCmd, databytes):
payload = '\x11\x00' + clusterCmd + databytes
zb.send('tx_explicit',
dest_addr_long = whereLong,
dest_addr = whereShort,
src_endpoint = srcEndpoint,
dest_endpoint = destEndpoint,
cluster = clusterId,
profile = profileId,
data = payload
)

def tryCommand():
# Try out commands here
print '********* Sending Test Command'
switchLongAddr ='\x00\x0d\x6f\x00\x03\xc2\x71\xcc'
if switchLongAddr in devices:
switchShortAddr = devices[switchLongAddr][0]
print ' Switch Status'
sendSwitch(switchLongAddr, switchShortAddr, '\x00', '\x02',
'\x00\xee', '\xc2\x16', '\x01', '\x01')
else:
print 'Waiting for short address'



#------------ XBee Stuff -------------------------
# this is the /dev/serial/by-id device for the USB card that holds the XBee
ZIGBEEPORT = "/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_A901QL3F-if00-port0"
ZIGBEEBAUD_RATE = 9600
# Open serial port for use by the XBee
ser = serial.Serial(ZIGBEEPORT, ZIGBEEBAUD_RATE)


# The XBee addresses I'm dealing with
BROADCAST = '\x00\x00\x00\x00\x00\x00\xff\xff'
UNKNOWN = '\xff\xfe' # This is the 'I don't know' 16 bit address

#-------------------------------------------------
logging.basicConfig()


# Create XBee library API object, which spawns a new thread
zb = ZigBee(ser, callback=messageReceived)
# create a queue to put the messages into so they can
# be handled in turn without one interrupting the next.
messageQueue = Queue.Queue(0)

# A dictionary to put devices into as they show up
devices = {}
scheditem = BackgroundScheduler()
scheditem.add_job(showDevices, 'interval', seconds=60)
#scheditem.add_job(tryCommand, 'interval', seconds=15)
scheditem.start()

print ("started")
notYet = True;
firstTime = True;
while True:
try:
if (firstTime):
# this is in case I need some initialization in the
# future
firstTime = False
if messageQueue.qsize() > 0:
#print "getting message"
message = messageQueue.get()
handleMessage(message)
messageQueue.task_done();

time.sleep(0.1)
#print ("tick") # This is here to let you know it's alive

sys.stdout.flush() # if you're running non interactive, do this

except IndexError:
print "empty line"
except NameError as e:
print "NameError:",
print e.message.split("'")[1]
except KeyboardInterrupt:
print ("Keyboard interrupt")
break
except:
print ("I didn't expect this error:", sys.exc_info()[0])
traceback.print_exc()
break
print ("After the while loop")
# halt() must be called before closing the serial
# port in order to ensure proper thread shutdown
scheditem.shutdown(wait=False) # shut down the apscheduler
zb.halt()
ser.close()

Yes, it's a bit complex. More complex than other hacks I've posted. I had to do this to allow for multiple devices that had multiple endpoints and clusters involved. It still isn't too hard to understand, and you should be fine when you start messing with it.

You'll notice that I keep a list of devices that have contacted the code. This is to allow you to tell what happened with various devices as the interaction continues. I log the long address, time and relevant details as they happen to help you understand how to deal with the incoming data. When you have several devices, the log can become a bit overpowering since nothing stops sending, it just goes on forever. Here's a bit of the logging so you can get an idea what it looks like:

started
1432598997 000d6f0004510782 clid 00f0-09 9e fb 1f bf 9e 7a 0a b7 0b c4 01 af f4 03 02
Door Sensor 85.982F
1432598998 000d6f0003cf0e5b clid 00ef-09 00 81 00 00
Current Usage: 0
1432598999 000d6f000283fa4e clid 00f0-09 a2 fb 1d 3c a2 e5 03 bc 0b 00 00 a7 fb 03 00
Key Fob 86.072F Counter 65380924
1432599000 000d6f00025886b3 clid 00ef-09 00 81 87 00
Current Usage: 135
1432599000 000d6f00025886b3 clid 00f0-09 00 fb 1c 22 dd 95 83 66 32 00 00 b5 b0 01 00
Power Switch
1432599000 000d6f000237b25a clid 00ef-09 00 81 01 00
Current Usage: 1
1432599003 000d6f000258a4cc clid 00ef-09 00 81 00 00
Current Usage: 0
1432599005 000d6f0002547a5d clid 00ef-09 00 81 00 00
Current Usage: 0
1432599007 000d6f0002547a5d clid 00f0-09 00 fb 1c 2b 09 b3 1a 66 32 00 00 b7 fe 01 00
Power Switch
1432599008 000d6f0003cf0e5b clid 00ef-09 00 81 00 00
Current Usage: 0
1432599009 000d6f00025886b3 clid 00ef-09 00 82 67 a4 a5 72 80 e5 20 01 00
Switch Minute Stats: Usage, 148 Watt Hours; Uptime, 18933120 Seconds
1432599010 000d6f00025886b3 clid 00ef-09 00 81 87 00
Current Usage: 135
1432599010 000d6f000237b25a clid 00ef-09 00 81 01 00
Current Usage: 1
1432599013 000d6f000258a4cc clid 00ef-09 00 82 f7 ea 21 00 b8 bc 03 00 00
Switch Minute Stats: Usage, 0 Watt Hours; Uptime, 244920 Seconds
1432599013 000d6f000258a4cc clid 00ef-09 00 81 00 00
Current Usage: 0
1432599015 000d6f0002547a5d clid 00ef-09 00 81 00 00
Current Usage: 0
1432599018 000d6f0003cf0e5b clid 00ef-09 00 81 00 00
Current Usage: 0
1432599020 000d6f00025886b3 clid 00ef-09 00 81 87 00
Current Usage: 135
1432599020 000d6f000237b25a clid 00ef-09 00 82 a9 3d 8a 1f 40 e9 20 01 00
Switch Minute Stats: Usage, 40 Watt Hours; Uptime, 18934080 Seconds
1432599020 000d6f0003cf0e5b clid 00f0-09 00 fb 1c 34 a9 6e 64 fb 31 00 00 ba b7 01 00
Power Switch
1432599020 000d6f000237b25a clid 00ef-09 00 81 01 00
Current Usage: 1
1432599023 000d6f000237b25a clid 00f0-09 00 fb 1c 25 0d a5 83 4a 32 00 00 b4 b4 01 00
Power Switch
1432599023 000d6f000258a4cc clid 00ef-09 00 81 00 00
Current Usage: 0
1432599025 000d6f0002547a5d clid 00ef-09 91 82 40 35 ff 00 d4 ac 06 00 00
Switch Minute Stats: Usage, 1 Watt Hours; Uptime, 437460 Seconds
1432599025 000d6f0002547a5d clid 00ef-09 00 81 00 00
Current Usage: 0
1432599025 000d6f000258a4cc clid 00f0-09 00 fb 1c 18 11 f3 0e 66 32 00 00 bd b3 01 00
Power Switch
1432599028 000d6f0003cf0e5b clid 00ef-09 00 81 00 00
Current Usage: 0
1432599030 000d6f00025886b3 clid 00ef-09 00 81 87 00
Current Usage: 135
1432599030 000d6f00025886b3 clid 00f0-09 00 fb 1c 22 55 96 83 66 32 00 00 b6 b6 01 00
Power Switch
1432599030 000d6f000237b25a clid 00ef-09 00 81 01 00
Current Usage: 1
1432599033 000d6f000258a4cc clid 00ef-09 00 81 00 00
Current Usage: 0
1432599035 000d6f0002547a5d clid 00ef-09 00 81 00 00
Current Usage: 0
1432599036 000d6f0002547a5d clid 00f0-09 00 fb 1c 2b 81 b3 1a 66 32 00 00 b7 fd 01 00
Power Switch
1432599038 000d6f0003cf0e5b clid 00ef-09 00 82 38 42 54 00 bc 1b 19 00 00
Switch Minute Stats: Usage, 0 Watt Hours; Uptime, 1645500 Seconds
1432599038 000d6f0003cf0e5b clid 00ef-09 00 81 00 00
Current Usage: 0
1432599040 000d6f00025886b3 clid 00ef-09 00 81 87 00
Current Usage: 135
1432599040 000d6f000237b25a clid 00ef-09 00 81 01 00
Current Usage: 1
1432599043 000d6f000258a4cc clid 00ef-09 00 81 00 00
Current Usage: 0
1432599045 000d6f0002547a5d clid 00ef-09 00 81 00 00
Current Usage: 0
1432599048 000d6f0003cf0e5b clid 00ef-09 00 81 00 00
Current Usage: 0
1432599050 000d6f00025886b3 clid 00ef-09 00 81 87 00
Current Usage: 135
1432599050 000d6f0003cf0e5b clid 00f0-09 00 fb 1c 34 21 6f 64 fb 31 00 00 ba bb 01 00
Power Switch
1432599050 000d6f000237b25a clid 00ef-09 00 81 01 00
Current Usage: 1
1432599053 000d6f000237b25a clid 00f0-09 00 fb 1c 25 85 a5 83 4a 32 00 00 b4 a4 01 00
Power Switch
1432599054 000d6f000258a4cc clid 00ef-09 00 81 00 00
Current Usage: 0
1432599055 000d6f0002547a5d clid 00ef-09 00 81 00 00
Current Usage: 0
1432599055 000d6f000258a4cc clid 00f0-09 00 fb 1c 18 89 f3 0e 66 32 00 00 bd b1 01 00
Power Switch
Known Devices ************
00 0d 6f 00 02 83 fa 4e : 92 26
00 0d 6f 00 04 51 07 82 : a9 3b
00 0d 6f 00 02 58 a4 cc : e1 3c
00 0d 6f 00 02 58 86 b3 : 27 f6
00 0d 6f 00 02 37 b2 5a : 2b d1
00 0d 6f 00 02 54 7a 5d : 41 c7
00 0d 6f 00 03 cf 0e 5b : 79 c5
**************************
1432599058 000d6f0003cf0e5b clid 00ef-09 00 81 00 00
Current Usage: 0
1432599060 000d6f00025886b3 clid 00ef-09 00 81 87 00
Current Usage: 135
1432599060 000d6f00025886b3 clid 00f0-09 00 fb 1c 22 cd 96 83 66 32 00 00 b6 c1 01 00
Power Switch

The first field is the Unix timestamp. Basically the number of seconds since 1970. The second is the long address of the device; this never changes. Then the cluster id followed by the data specific to the cluster. I decoded the times for the key fob and the temperature for the door switch because those could be fun to play with. Notice that the smart switches really send a lot of data, this will help you understand why I limit the number of devices with a test at the top of the code. If everything is there, you have a heck of a time picking out what you want to see. The list of know devices is the long address and the currently assigned short address. The short address will change from time to time as you restart the process or pull a battery somewhere.

Remember, this runs on a Raspberry Pi, I haven't tried it on anything else. I have my XBee plugged into a USB port on the Pi, if you're using the serial port, just change the port in the code, that should be all that is needed. If you don't know which usb port to use, there's a ton of articles on the web that will help you find the port, just look around a bit, or to see how I did it look here <link>. What I'm hoping for is a couple of folk that want to use these nicely priced and well made devices with their own code for some project to grab it and give me some feedback on the interaction. It would be nice to see someone else running this code.

Have fun.

Viewing all articles
Browse latest Browse all 218

Trending Articles