Saturday, May 9, 2015

Hacking Into The Iris Door Sensor

Several people have asked me about the Iris Door Sensor, and I haven't had much interest. My battery operated switch can do this and is totally programmable, but I figured, "What the heck." I went ahead and got one when I went to Lowe's to get another smart switch.

Now, this is one of those projects that will go on and on as I try to get it to work. Meanwhile, these are the first steps.

This is what one looks like:


I don't think Lowe's will mind that I stole the picture from their site. First thing I did was open it up and look inside:


The spot on the left is for the battery and there's a switch for joining it to a network. I tried to get it to join with my Iris network, but it didn't work, so being me, I took it apart. Here's the circuit board removed from the casing:


Not much on this side, so flipping it over:


This is a side view of the magnet it comes with:


Remember those little extrusions on the magnet case; I'll talk about them later. I got out the magnifying glass and took a look at the active components, the list isn't very long:

Battery - CR2 Lithium, 3V
Magnetic Reed Switch - Hamlin 59050-1-S-00-0 Normally Open
Magnet off center to be near the reed switch.
Antenna - Antenova a6150
Processor - Silicon Labs Ember 250
Temperature Sensor - Microchip MCP9801M

Nice set of high quality components. this thing is put together pretty well. The important part is the Ember 250 SOC (system on a chip) that runs the device. These are made specifically for ZigBee and are very nice devices; to bad the product comes from Alertme. They always ignore the ZigBee spec and go their own way in critical areas. This is going to make it tough to hack into.

I put together some code and turned off my normal network to keep down interference and gave it a try. I couldn't get it to join properly, but I was able to see it trying. Since this is an Alertme device, it has significant API differences from the ZigBee spec, so I started trying things. I got it to join eventually by a non-reproducible accident and saw it running.

The problem is that I can't get that to happen again. When it was working, I noticed that it was really sensitive to where the magnet was, so I looked at the reed switch specs and it's only got a 7 mm range. That means that the magnet needs to be right next to the sensor for it to close. The FAQ for the switch on the Lowe's site even talks about that. They blame it on polarity when it's really the fact that the magnet needs to be so close to the sensor; like polarity would matter on a reed switch. A neo magnet might make the range longer, but it wouldn't be as pretty.

Some of us may have problems with that on installation; seven millimeters is not very much, especially when it's about 4 mm from the switch to the case. Do you see why they have those little protusions on the case now? It gives some spacing and a little place for the magnet to rub on the sensor case when the door closes. Strange way to build it, but I'm not an engineer.

About how it works: When the battery is first inserted, it starts sending Device Announce messages and continues this until I answer it with an Active Endpoint  Request (which it hasn't answered yet). When it gets the Active Endpoint, it sends a Match Descriptor request and I answer it with a Match Descriptor Response tailored to it. Then, it sends one of the odd Alertme messages to profile 0xC216 that I've seen before from Alertme devices.

This is the point where, with the smart switches, I send a set of canned responses back and the device would join. The door switch doesn't join, It will stay around for a while trying, but will eventually give up and move on to another channel to look for another controller.

The time I got it to join, it started sending messages to cluster 0x500, the security device cluster. While it was doing that I could actually see the switch change state because the values it sent back were changing based on magnet location. However, it was erratic and extremely slow at sensing the magnet. There was some LED flashing on the device that seemed to correspond to the position of the magnet, but I couldn't be sure.

I'm going to post the code I'm working with as it is right now, but it's not pretty. I hacked up a piece of code I developed to break into another ZigBee device and am making experimental changes to it trying to understand this door switch.

If you grab it, there's an 'if' statement up at the top where I get the packets from the XBee, I check the incoming source address and skip anything that isn't coming from the switch. I have a number of the Iris devices and they were messing up the experiment, so I just ignore them in the code. You'll have to change that to your own devices address.

#! /usr/bin/python
# Have fun

from xbee import ZigBee 
import logging
import datetime
import time
import serial
import sys, traceback
import shlex
from struct import *
'''
Before we get started there's a piece of this that drove me nuts.  Each message to a 
Zigbee cluster has a length and a header.  The length isn't talked about at all in the 
Zigbee documentation (that I could find) and the header byte is drawn backwards to
everything I've ever dealt with.  So, I redrew the header byte so I could understand
and use it:

7   6   5   4   3   2   1   0
            X                   Disable Default Response 1 = return default message
                X               Direction 1 = server to client, 0 = client to server
                    X           Manufacturer Specific 
                            X   Frame Type 1 = cluster specific, 0 = entire profile
                                    
So, to send a cluster command, set bit zero, and to get an attribute from a cluster
set bit 4.  If you want to be sure you get a reply, set the default response.  I haven't
needed the manufacturer specific bit yet.

'''

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 getAttributes(data, thisOne):
    ''' OK, now that I've listed the clusters, I'm going to see about 
    getting the attributes for one of them by sending a Discover
    attributes command.  This is not a ZDO command, it's a ZCL command.
    ZDO = ZigBee device object - the actual device
    ZCL = Zigbee device cluster - the collection of routines to control it.
        Frame control field, bit field,  first byte
            bits 0-1, frame type field
                00 entire profile
                01 specific to a particular cluster
                10-11 reserved (don't use)
                Note, if you're sending commands, this should be 01
                if you're reading attributes, it should be 00
            bit 2, manufacturer specific, if this bit is set, include
                   the manufacturer code (below)
            bit 3 direction, this determines if it is from a client to 
                   server.  
                 1 server to client
                 0 client to server
                 Note, server and client are specific to zigbee, not
                 the purpose of the machine, so think about this.  For
                 example to turn an on/off switch on, you have to be the
                 server so this bit will be 01
            bit 4 disable default response
            bits 5-7 reserved (set to zero)
        Manufacturer code,  either 2 bytes or not there 
        Transaction sequence number, byte
        Command identifier, byte
        Frame payload,  variable, the command bytes to do something
        
        frame control bits = 0b00 (this means a BINARY 00)
        manufacturer specific bit = 0, for normal, or one for manufacturer
        So, the frame control will be 0000000
        discover attributes command identifier = 0x0c
        
        then a zero to indicate the first attribute to be returned
        and a 0x0f to indicate the maximum number of attributes to 
        return.
    '''
    print "Sending Discover Attributes"
    zb.send('tx_explicit',
        dest_addr_long = data['source_addr_long'],
        dest_addr = data['source_addr'],
        src_endpoint = '\x00',
        dest_endpoint = '\x01',
        cluster = thisOne, # cluster I want to know about
        profile = '\x01\x04', # home automation profile
        # means: frame control 0, sequence number 0xaa, command 0c,
        # start at 0x0000 for a length of 0x0f
        data = '\x00' + '\xaa' + '\x0c'+ '\x00' + '\x00'+ '\x0f'
        )

# this is a call back function.  When a message
# comes in this function will get the data
def messageReceived(data):
    
    try:
        # This is the long address of my door switch device
        # since I have several other devices and they are transmitting
        # all the time, I'm excluding them and only allowing the
        # door switch in
        if data['source_addr_long'] != '\x00\x0d\x6f\x00\x03\xc2\x71\xcc':
            return
            
        print 'gotta packet',
        #print data
        if (data['id'] == 'rx_explicit'):
            print "RF 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']=='\x01\x04'): # Home Automation Profile
                # This has to be handled differently than the general profile
                # each response if from a cluster that is actually doing something
                # so there are attributes and commands to think about.
                #
                # Since this is a ZCL message; which actually means this message is 
                # is supposed to use the ZigBee cluster library to actually do something
                # like turn on a light or check to see if it's on, the command way down
                # in the rf_data is important.  So, the commands may be repeated in
                # each cluster and do slightly different things
                #
                # I'm going to grab the cluster command out of the rf_data first so 
                # I don't have to code it into each cluster
                #print "take this apart"
                #print repr(data['rf_data'])
                if (data['rf_data'][0] == '\x08'): # was it successful?
                    #should have a bit check to see if manufacturer data is here
                    cCommand = data['rf_data'][2]
                    print "Cluster command: ", hex(ord(cCommand))
                else:
                    print "Cluster command failed"
                    return
                # grab the payload data to make it easier to work with
                payload = data['rf_data'][3:] #from index 3 on is the payload for the command
                datatypes={'\x00':'no data',
                            '\x10':'boolean',
                            '\x18':'8 bit bitmap',
                            '\x20':'unsigned 8 bit integer',
                            '\x21':'unsigned 24 bit integer',
                            '\x30':'8 bit enumeration',
                            '\x42':'character string'}
                print "Raw payload:",repr(payload)
                # handle these first commands in a general way
                if (cCommand == '\x0d'): # Discover Attributes
                    # This tells you all the attributes for a particular cluster
                    # and their datatypes
                    print "Discover attributes response"
                    if (payload[0] == '\x01'):
                        print "All attributes returned"
                    else:
                        print "Didn't get all the attributes on one try"
                    i = 1
                    if (len(payload) == 1): # no actual attributes returned
                        print "No attributes"
                        return
                    while (i < len(payload)-1):
                        print "    Attribute = ", hex(ord(payload[i+1])) , hex(ord(payload[i])),
                        try:
                            print datatypes[payload[i+2]]
                            i += 3
                        except:
                            print "I don't have an entry for datatype:", hex(ord(payload[i+2]))
                            return
                            
                if (clusterId == 0x0000): # Under HA this is the 'Basic' Cluster
                    pass
                elif (clusterId == 0x0003): # 'identify' should make it flash a light or something 
                    pass
                elif (clusterId == 0x0004): # 'Groups'
                    pass
                elif (clusterId == 0x0005): # 'Scenes'  
                    pass
                elif (clusterId == 0x0006): # 'On/Off' this is for switching or checking on and off  
                    print "inside cluster 6"
                
                elif (clusterId == 0x0008): # 'Level'  
                    pass
                else:
                    print("Haven't implemented this yet")
            elif (data['profile']=='\x00\x00'): # The General 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 == 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'
                    printData(data)
                    
                    print "sending Active Endpoint Request "
                    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',
                        # The first item is a number to identify the message
                        # The next two are the short address of the device
                        data = '\x12' + data['source_addr'][1]+ data['source_addr'][0]
                    )

                    
                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)
                    if (ord(data['rf_data'][1]) == 0): # this means success
                        print "Active Endpoint reported back is: {0:02x}".format(ord(data['rf_data'][5]))
                        
                    print("Now trying simple descriptor request on an endpoint")
                    zb.send('tx_explicit',
                        dest_addr_long = data['source_addr_long'],
                        dest_addr = data['source_addr'],
                        src_endpoint = '\x00',
                        dest_endpoint = '\x00', # This has to go to endpoint 0 !
                        cluster = '\x05\x00', #simple descriptor request'
                        profile = '\xc2\x16',
                        data = '\x13' + data['source_addr'][1] + data['source_addr'][0] + '\x01'
                    )
                elif (clusterId == 0x8004):
                    print "simple descriptor response"
                    try:
                        clustersFound = []
                        r = data['rf_data']
                        if (ord(r[1]) == 0): # means success
                            #take apart the simple descriptor returned
                            endpoint, profileId, deviceId, version, inCount = \
                                unpack('<BHHBB',r[5:12])
                            print "    endpoint reported is: {0:02x}".format(endpoint)
                            print "    profile id:  {0:04x}".format(profileId)
                            print "    device id: {0:04x}".format(deviceId)
                            print "    device version: {0:02x}".format(version)
                            print "    input cluster count: {0:02x}".format(inCount)
                            position = 12
                            # input cluster list (16 bit words)
                            for x in range (0,inCount):
                                thisOne, = unpack("<H",r[position : position+2])
                                clustersFound.append(r[position+1] + r[position])
                                position += 2
                                print "        input cluster {0:04x}".format(thisOne)
                            outCount, = unpack("<B",r[position])
                            position += 1
                            print "    output cluster count: {0:02x}".format(outCount)
                            #output cluster list (16 bit words)
                            for x in range (0,outCount):
                                thisOne, = unpack("<H",r[position : position+2])
                                clustersFound.append(r[position+1] + r[position])
                                position += 2
                                print "        output cluster {0:04x}".format(thisOne)
                            clustersFound.append('\x0b\x04')
                            print "added special cluster"
                            print "Completed Cluster List"
                    except:
                        print "error parsing Simple Descriptor"
                        printData(data)
                    print repr(clustersFound)
                    for c in clustersFound:
                        getAttributes(data, c) # Now, go get the attribute list for the cluster
                elif (clusterId == 0x0006):
                    print "Match Descriptor Request"
                    # Match Descriptor Request
                    printData(data)
                    # Now the Match Descriptor Response
                    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',
                        #seq #, status, address,num endp, list
                        data = '\x00\x00\x00\x00\x01\x02')
                else:
                    print ("Unimplemented Cluster ID", hex(clusterId))
                    print
            elif (data['profile']=='\xc2\x16'): # Alertme Specific
                printData(data)
                '''print "Sending weird messages"
                if (clusterId == 0x00f6):
                    payload3 = '\x11\x01\x01'
                    zb.send('tx_explicit',
                        dest_addr_long = data['source_addr_long'],
                        dest_addr = data['source_addr'],
                        src_endpoint = '\x00',
                        dest_endpoint = data['source_endpoint'],
                        cluster = data['cluster'],
                        profile = '\xc2\x16',
                        data = payload3
                        )
                    payload4 = '\x19\x01\xfa\x00\x01'
                    zb.send('tx_explicit',
                       dest_addr_long = data['source_addr_long'],
                       dest_addr = data['source_addr'],
                       src_endpoint = '\x00',
                       dest_endpoint = data['source_endpoint'],
                       cluster = data['cluster'],
                       profile = '\xc2\x16',
                       data = payload4
                    )'''
                    
                if (clusterId == 0x00ef):
                    pass
                elif (clusterId == 0x00f0):
                    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 sendSwitch(whereLong, whereShort, srcEndpoint, destEndpoint, 
                clusterId, profileId, clusterCmd, databytes):
    
    payload = '\x11\x00' + clusterCmd + databytes
    # print 'payload',
    # for c in payload:
        # print hex(ord(c)),
    # print
    # print 'long address:',
    # for c in whereLong:
        # print hex(ord(c)),
    # print
        
    zb.send('tx_explicit',
        dest_addr_long = whereLong,
        dest_addr = whereShort,
        src_endpoint = srcEndpoint,
        dest_endpoint = destEndpoint,
        cluster = clusterId,
        profile = profileId,
        data = payload
        )

#------------ 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'
theSwitch = '\x00\x0d\x6f\x00\x03\x58\x05\xc2'
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)

print ("started")
notYet = True;
firstTime = True;
while True:
    try:
        time.sleep(5)
        if (firstTime):
#           sendSwitch(whereLong=theSwitch, whereShort=UNKNOWN, srcEndpoint='\x00', 
#                   destEndpoint='\x00', clusterId='\x00\x00', profileId='\x00\x00', 
#                   clusterCmd='\x00', databytes='\x00')
#            print "sending Active Endpoint Request "
#            zb.send('tx_explicit',
#                dest_addr_long = theSwitch,
#                dest_addr = '\x94\x65',
#                src_endpoint = '\x00',
#                dest_endpoint = '\x00',
#                cluster = '\x00\x05',
#                profile = '\x00\x00',
#                # The first item is a number to identify the message
#                # The next two are the short address of the device
#                data = '\x12' + '\x65' + '\x94'
#            )
#            time.sleep(10)
#            print "sending 'configure reporting'"
#            zb.send('tx_explicit',
#                dest_addr_long = theSwitch,
#                dest_addr = '\x94\x65',
#                src_endpoint = '\x00',
#                dest_endpoint = '\x01',
#                cluster = '\x00\x06', # cluster I want to deal with
#                profile = '\x01\x04', # home automation profile
#                data = '\x00' + '\xaa' + '\x06' + '\x00' + '\x00' + '\x00' + '\x10' + '\x00' + '\x00' + '\x00' + '\x40' + '\x00' + '\x00'
#            )

            firstTime = False
        print ("tick")
        
        sys.stdout.flush() # if you're running non interactive, do this

    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
zb.halt()
ser.close()

I told you it wasn't pretty, so stop complaining.

I'm continuing to play with this switch as time, and new ideas permit, so I think the door switch can be conquered. However, if is as flaky as the initial results would indicate, I may not be using it. Having to have the magnet right up against the sensor and having so little elbow room would stop the switch from being useful on things like garage doors, doors that move due to weather or settling, wooden windows that may not shut all the way, that kind of thing. If someone has one that works well in an Iris setup can tell us about this switch, it would be great.

If you want, grab the code, experiment, play, and let me know the results you get.

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

11 comments:

  1. Didn't you get a Razberry a while back? I've never looked back myself, just look for better ways to do things. Have made several program changes as I've learned new and better ways to do things.

    ReplyDelete
    Replies
    1. I did get a razberry!! It's installed on a Pi and doing absolutely nothing. My plan is to get into that bad boy this summer and control a couple of switches, then expand it as it goes.

      But, for some reason it just galls me that companies like Alertme produce this stuff and claim compatibility when they clearly don't have it. The point may be moot soon though, they got bought out and it's unclear what's going to happen going forward. The evil little boy in me comes out each time I see one of their devices.

      Companies like Centralite make compliant Zigbee devices and controllers, but just haven't been able to penetrate the market.

      Z-wave is definitely the way of the future, and the company that makes this is a good one.

      Delete
  2. I can add that, once you fiddle with this switch enough to get it working, you have a fully functional switch that works fine until you stop giving it closures to report for about 8 seconds, then it leaves the network again.

    The pushbutton on the board may be used for forcing the device to forget a network and start over, but it's also definitely a tamper switch, and there is a bit in the attribute for this point that will set/clear when you start to pull the case apart (when it's working).

    I would love to be able to hijack this device, since I have about 8 of them on the IrisSmartHome network, but if it doesn't work out I'll have to chalk them up and look for another xbee-based contact sensor solution, so I can ditch Iris.





    ReplyDelete
    Replies
    1. It looks like Zigbee is taking a back seat to Z-Wave. Although Z-Wave is a closed protocol and very expensive to get into, there are ways of using it without breaking the protocol.

      The really annoying thing about Zigbee is that the consortium didn't enforce their rules about meeting the spec and the manufacturers went their own way.

      I haven't given up on this switch, but it is taking some time.

      Delete
    2. I think some Zigbee implementers took advantage of 'manufacturer specific' fields in the protocol headers that allow for customizations that make back-engineering really difficult.

      I didn't think much about getting any z-wave products but that was back when I was buying canned systems and they were expensive. Now it looks like I should be picking up one of these Razberry units...

      Delete
  3. I replaced my X10 devices with Z-Wave quite a few years ago and never looked back. At first Misterhouse was the controller, but eventually I went with a Vera Lite on UI5, their older OS.

    The trouble is, Z-Wave doesn't do everything I want. So XBees fill in the gap. Stealing ideas from Dave, OpenSprinker and some of my own, I put together a Raspberry Pi as an XBee network gateway controller. Commands go back and forth between the Pi and the Vera.

    An example of how it works is, I have a strip of LED lights along my stairway. It is controlled by an Arduino with XBee and a motion sensor. If I approach it from the bottom of the stairs, the Arduino senses the motion and turns on the LED lights. If I approach the stairs from the upper lever, a Z-Wave motion sensor is triggered, the Vera Lite sends a command to the Raspberry Pi, which in turn send the command to the Arduino controller, which turns on the lights. When no more motion is detected, the Arduino code times out and the lights go out.

    ReplyDelete
    Replies
    1. That is totally AWESOME !

      Delete
    2. This describes my house. I have a growing number of Arduino devices that communicate with the RPi using XBee. The RPi has control over them, but they will operate in a default mode without input from the main controller. The Z-Wave stuff comes in and it's one fun event after another. I have a door sensor that refuses to cooperate so it's getting retired in favor of a Fibaro unit. They seem to be a much better door sensor...

      Delete
  4. Have you made any progress on this? the Iris devices are nice and cheap, but I hate the closed and unreliable Iris hub and service.

    ReplyDelete
    Replies
    1. Yep, I have a couple of the contact switches running and occasionally use the key fob. My partner in doing this has a full system running and actually turned off his paid portion of the Iris service. He let the free portion alone since it's ... well free.

      It's not perfect though. Since he only has one router, if he moves it, he has to set the network up again. I have more routers than switches, so it doesn't affect me that way.

      All you have to do is follow the trail of postings on it. I have enough code on line to get you started in bringing up your own system any way you would like to do it. At some point in the distant future (months), I plan on revisiting this and trying to get the dependence on a router out of the picture, but that's going to be a lot of head scratching, and I've got summer stuff to do.

      Delete