Tuesday, June 23, 2015

Hacking Into The Iris Door Sensor, Part 4, Resolution

The previous post on this project is here <link>.

Well, my partner in questionable activity in hacking the Iris Contact Switch and Key Fob has gotten his devices to work as well as mine, so it's time to close this project off for a while.

The reason my setup worked well and his didn't was because he didn't have a router on his network of Iris devices and I did. The Iris Smart Switch is a router and I have a handful of them scattered around the house. When he plugged one in and tried the devices, his started working and away he went. I just got word that he is switching his network over to completely local control.

Why is a router necessary? Frankly, I'm not sure at this point and I'll check into it more over time, but I have a couple of door switches working quite well, and the key fob controlling one of my smart switches. The system works nicely.

Between the two of us we managed to decode the various timers and such so you folk can pick up where we left off and implement these little devices in your own home for whatever you want. Nicely made (physically) product that was hampered only by the special code Iris put in them to force you to use their hub. I don't know why manufacturers insist on doing that, especially since there are folk like me that will take it as a challenge.

Here's the latest code with the various items. It will support the Iris smart switch, Key Fob and Door Switch. The code doesn't save status to a data base, or forward it to anything else, it just joins the devices and watches them; you'll need to adapt it to what you want to do.

#! /usr/bin/python
'''
Hacking into the iris door sensor
Have fun
'''
from xbee import ZigBee 
import datetime
import time
import serial
import sys, traceback
import shlex
import Queue
from struct import *
import binascii
import inspect


# line number for debugging
def getLineNumber():
    return inspect.stack()[1][2]
    
# show data formatted so I can read it
def showData(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 showClusterData(lAddr,sAddr, clusterId, data):
    print int(time.time()),
    print "".join("%02x" % ord(b) for b in lAddr) + \
        " " + \
        "".join("%02x" % ord(b) for b in sAddr) + \
        " clid "+"%04x" % clusterId + "-" + \
        "".join("%02x " % ord(b) for b in data)

# this is a call back function for XBee receive. 
# 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:
#        if data['source_addr_long'] not in \
#            ['\x00\x0d\x6f\x00\x04\x51\x07\x82',]:
#            return
        #print 'gotta packet' 
        #showData(data)
        if (data['id'] == 'rx_explicit'):
            #print "RX Explicit"
            #showData(data)
            clusterId = (ord(data['cluster'][0])*256) + ord(data['cluster'][1])
            #print 'Cluster ID:', hex(clusterId),

            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")
                    #showData(data)
                elif (clusterId == 0x0004):
                    # Simple Descriptor Request, 
                    print("Simple Descriptor Request")
                    #showData(data)
                elif (clusterId == 0x0005):
                    # Active Endpoint Request, 
                    print("Active Endpoint Request")
                    #showData(data)
                elif (clusterId == 0x0006):
                    print "Match Descriptor Request"
                    '''
                    the switch looks for clusters under profile
                    c216, and I respond with only 1 cluster 02
                    '''
                    showData(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 = data['rf_data'][0:1] + '\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 == 0x13):
                    # 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
                    )
                elif (clusterId == 0x8000):
                    print("Network (16-bit) Address Response")
                    #showData(data)
                elif (clusterId == 0x8005):
                    # this is the Active Endpoint Response This message tells you
                    # what the device can do, but it isn't constructed correctly to match 
                    # what the switch can do according to the spec.  This is another 
                    # message that gets it's response after I receive the Match Descriptor
                    print 'Active Endpoint Response'
                # elif (clusterId == 0x0006):
                elif (clusterId == 0x8038):
                    print("Management Network Update Request");
                else:
                    print ("Unimplemented Cluster ID", hex(clusterId))
                    print
            elif (data['profile']=='\xc2\x16'): # Alertme Specific
                if (clusterId == 0xee):
                    clusterCmd = ord(data['rf_data'][2])
                    status = ''
                    if (clusterCmd == 0x80):
                        if (ord(data['rf_data'][3]) & 0x01):
                            status = "ON"
                        else:
                            status = "OFF"
                elif (clusterId == 0xef):
                    clusterCmd = ord(data['rf_data'][2])
                    status = data['rf_data'] # cut down on typing
                    if (clusterCmd == 0x81):
                        usage = unpack('<H', status[3:5])[0]
                    elif (clusterCmd == 0x82):
                        usage = unpack('<L', status[3:7])[0] / 3600
                        upTime = unpack('<L', status[7:11])[0]
                        #print ("%s Minute Stats: Usage, %d Watt Hours; Uptime, %d Seconds" %(name, usage/3600, upTime))
                elif (clusterId == 0xf0):
                    showClusterData(data['source_addr_long'],data['source_addr'],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",
                            if ord(status[-1]) & 0x01 == 1:
                                print "reed switch open",
                            else:
                                print "reed switch closed",
                            if ord(status[-1]) & 0x02 == 0:
                                print "tamper switch open",
                            else:
                                print "tamper switch closed",
                            
                        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':
                            # This indicates a door sensor
                            # with an invalid temperature reading
                            # the other items are OK 
                            print " Door Sensor",
                            print "Temperature invalid",
                            if ord(status[-1]) & 0x01 == 1:
                                print "reed switch open",
                            else:
                                print "reed switch closed",
                            if ord(status[-1]) & 0x02 == 0:
                                print "tamper switch open",
                            else:
                                print "tamper switch closed",
                            #This may be the missing link to this thing
                            print 'sending missing link',
                            zb.send('tx_explicit',
                               dest_addr_long = data['source_addr_long'],
                               dest_addr = data['source_addr'],
                               src_endpoint = data['dest_endpoint'],
                               dest_endpoint = data['source_endpoint'],
                               cluster = '\x00\xf0',
                               profile = '\xc2\x16',
                               data = '\x11\x39\xfd'
                            )
                            pass
                        else:
                            print " Don't know this device yet",
                        print ''
                    else:
                        print " Unknow cluster command"
                        print ''
                    pass
                elif (clusterId == 0x00f2):
                    showClusterData(data['source_addr_long'],data['source_addr'],clusterId,data['rf_data'])
                    print 'Tamper Switch Changed State to',
                    status = data['rf_data'] 
                    if ord(status[3]) == 0x02:
                        print "Open",
                    else:
                        print "Closed",
                    print ''
                    pass
                elif (clusterId == 0x00f3):
                    showClusterData(data['source_addr_long'],data['source_addr'],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 == 0xf6):
                    showClusterData(data['source_addr_long'],data['source_addr'],clusterId,data['rf_data'])
                    print ''
                    print "Identify Message"
                    #extract vendor strings
                    v = data['rf_data']
                    vendorstr = " - Vendor:"
                    start = 21
                    datalen=len(v)
                    while(start < datalen):
                        slen=ord(v[start])
                        vendorstr = vendorstr + " " + v[start+1:start+1+slen]
                        start = start+slen+1
                    print vendorstr
                    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 == 0x0500): # This is the security cluster
                    showClusterData(data['source_addr_long'],data['source_addr'],clusterId,data['rf_data'])
                    showData(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 = data['dest_endpoint'],
                            dest_endpoint = data['source_endpoint'],
                            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 Cluster ID", hex(clusterId))
                    print
            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 stopXBee():
    print("XBee stop handler")
    zb.halt()
    ser.close()

####################### Actually Starts Here ################################    
#------------ 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 = 57600
# 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

# create a queue to put the messages into so they can
# be handled in turn without one interrupting the next.
messageQueue = Queue.Queue(0)

# Create XBee library API object, which spawns a new thread
zb = ZigBee(ser, callback=messageReceived)
print "started"
while True:
    try:
        if messageQueue.qsize() > 0:
            #print "getting message"
            message = messageQueue.get()
            handleMessage(message)
            messageQueue.task_done();
            sys.stdout.flush() # if you're running non interactive, do this
    except KeyboardInterrupt:
        print "Keyboard interrupt"
        zb.halt()
        ser.close()
        break
    except:
        print "Unexpected error:", sys.exc_info()[0] 
        traceback.print_exc()
        break

print ("After the while")
# just in case
zb.halt()
ser.close()

Remember, for this to work properly, you'll need one of the Smart Switches in the network, but the Smart Switch that can measure power usage as well as control it was what got me into looking at these devices in the first place. Remember to look at the previous posts on this project as well, I may have forgotten to mention something.

The piece that is still missing is support for an accelerometer that is inside the key fob. I don't have a clue how to initialize and use it. I don't need it, it would just be nice to understand. Maybe after I get some of the other things I'm working on done I'll come back and take another look.

Have fun.

17 comments:

  1. Note on the Lowes-Iris contact sensors: they are online at Lowes.com for $12.50, which is unbeatable for the price, even if you only use them as remote temperature sensors. Still marked $19.99 in the store, but they will have to sell them for the online price.

    ReplyDelete
    Replies
    1. That was pointed out to me a few days ago, and it really surprised me.You can't match the component costs at that price, it would be worth it for the plastic and such. My problem is I spent my money and can't justify a dozen of them or so right now. I hope the sale lasts a while.

      Delete
  2. Thanks for all the info and code. I was able to get my iris zigbee stuff up and running in under a day.

    ReplyDelete
    Replies
    1. Good job! That's the quickest anyone has been able to do it.

      Delete
  3. Out of curiosity, this works using the original v1 Iris Door/Window sensor correct? Do you know if this will also work with the newer sensor that they've released?

    ReplyDelete
    Replies
    1. I don't know ... yet. I haven't had the time to pick up one of the new devices to see. I suspect it will not work without some changes. I've been told that the V2 Iris devices are truly compliant with the ZigBee home automation spec. If it is, this will not work because it has the crap in it for the AlertMe specific stuff and very little of the HA stuff.

      However, I've already worked with the proper spec on other devices, so it should be (relatively) easy to incorporate enough into this code to make the new V2 devices work.

      I still have a bunch of other things going on, so I won't get to try this for a while.

      Delete
  4. One more question for you. I've read throughout your blogs about the XBee library you're using from Andrew Rapp. Do you know if his updated github Arduino library has the changes you've made to it or is it still better to grab whatever you've done?

    ReplyDelete
    Replies
    1. I honestly don't know. Once I made my own, I added debugging I needed and understand where things are in the code, so I haven't looked back.

      Delete
    2. Makes sense! I've seen dropbox or other links to the library that you've posted but I've also read so many of your posts that I don't know where the link is. Any chance you have that handy somewhere so I can grab it?
      Thanks!

      Delete
  5. I know this is old, but when you say "smart switch" what do you mean?

    ReplyDelete
  6. The faithful light switch on the wall that just goes click is a switch. Beyond that a switch that has a method of communication such that it can be controlled remotely and also send data on its state can be called smarter. AKA, smart switch.

    ReplyDelete
  7. I am really interested in the 2nd gen contact sensor switch with the hall effect "switch" instead of the mercury switch because I too want to use existing wired mercury switch system. If we can hack the software can we tell it to look for the connection open/close from existing wired mercury switches on one of the GPIO? doing away with or adding to the hall effect? Sorry for Anonymous, I Clark will create an account soon and post my other findings. Thanks

    ReplyDelete
    Replies
    1. I haven't had the time to play with the second gen Iris devices. Other folk have and they tell me that the devices are a lot like the Centralab devices I've already worked with. If you want to play with that code, it's here on the blog somewhere.

      Delete
  8. I just want to say thanks for all the data.
    Just successfully redid in C# and the data looks awesome. Took me a couple weeks of r&das I no nothing of python and my s2b pro chips act funny sometimes. they won't do anything until I find them in XCTU.(still investigating) I'm going to try it on windows IOT.

    Just would like to add something about temperature.
    My temperature from the switches just wasn't reading right from bytes 8 and 9 so i looked at my chip and the temperature IC is a 9801M from Microchip. They say the way to convert is the two bytes to int then * 2^-4 (.0625). I searched all items in the payload and since it was cold outside i put one sensor outside an one inside. The data checked out so I modified the algorithm a bit:
    (r = received frame object)

    var ta = new byte[] { r.Payload[10], r.Payload[11] };
    var fl = BitConverter.ToInt16(ta, 0);
    var d = fl * 0.0625;
    double f = ((double)d) * 1.8 + 32.0;

    Hope it helps someone.

    Thanks again,
    Al

    ReplyDelete
  9. Hey Dave! I'm your old partner in questionable activity. What are you using for door sensors these days? I got rid of the Iris stuff long ago, if you remember, and ended up getting an RTL-SDR (on Raspberry PI) to talk to these cheap Honeywell 8200 sensors. Worked great for 3 years, and it crapped out. Turns out you can't buy them anymore. Any other good options out there? Surely you've updated since the Iris days?

    ReplyDelete
    Replies
    1. Oh, I've updated almost everything except the door sensors. Like you, I tossed the Iris ones in a box and left them there. I think I could still find the box somewhere in the mess of old equipment.

      I got tired of the expensive batteries those things took. Living at the end of a dirt road, I really don't need them yet, but they were a challenge I couldn't pass up messing with.

      You raise a good question though: In this day and age, what would make a good door sensor that doesn't rely on the internet or a cloud service in any way?

      I've had really good luck with a handful of Tuya room temperature sensors. I've been gathering data on them to make sure they're reliable and have enough for a blog post. Net, these things are really good. I don't know enough about battery usage or how long they will really last on a battery, but they use those CR2032 batteries that are easy to get and cost about 20 cents (US).

      Based on that, and the way zigbee end devices work, I'm going to try their door sensors. Zigbee is just made for my purposes and everything from Tuya that I've tried works with the Hubitat Hub. The door sensors are end devices, so you have to have a zigbee router in place for them to work when the range exceeds the Hub. That's easy though, just get a light switch of some kind; either an in-wall switch or a wall plug would do it.

      ZWave might work as well, but I settled on Zigbee because I understand it, and listening to some of the tales of woe from some people about how hard it is to keep a ZWave network up, I may not try those out.

      Besides, I have a very robust Zigbee network already running and trust it.

      Should I try them?

      Delete
    2. I realized what time of year it is and the sale is going on. I ordered the sensors and got the discount while it was available.

      It'll be a month or more before they come in and maybe longer if I happen to hit the Chinese New Year holiday, but I've got plenty to do in the meantime.

      I just changed a shower faucet and have a wall to repair and about 30 other items to deal with while I wait.

      Check back and I'll tell you all about it.

      Delete