Wednesday, March 5, 2014

I Want To Complain about 'Experts'

My refrigerator started giving me trouble.  No, it wasn't the Iris Smart Switch I installed in the power, it was that the darn thing started to get warm.  I opened the door and the little thermometer I have inside said it was 55 F inside.  It usually runs under 45, so something was up.  I checked all the obvious things, the compressor was going, the fan on the condenser coil was running; I couldn't see anything obvious.

Once again, a little bit about the refrigerator:  It's a refrigerator (only) that I've had for some years and it has given me trouble before; I even had to have the evaporator coil replaced.  I got this thing because I wanted it to be the last refrigerator I'd ever buy.  To meet that I got the very top of the line GE Monogram model ZIRS36 in stainless steel (ZIRS36NMRH).  It's a really, really old design that doesn't even have a frost free option.  That's because it has no freezer inside and just doesn't form frost.

Basically, the power comes in, goes to the temperature control, then to a temperature sensor on the evaporator coil (to prevent freezing up) and then to the compressor.  There's three fans: one on the top to cool the condenser and condenser coil, one inside to circulate air from the evaporator coil to the rest of the fridge, and one on the bottom to evaporate any water that drains out of the evaporator.  Then the lights are all that's left.  Not much to go bad, and not much to troubleshoot when it has problems.

However, I couldn't find anything wrong with any of those things, so I went to the web and found this discussion on one of those 'Ask an Expert' sites <link>; basically, the expert said the correct thing first, then corrected himself to the wrong thing.  This fridge is a middle level cooling system.  It doesn't cool below freezing anywhere inside except the actual surface of the evaporator coil.  It maintains a temperature of about 38 degrees and doesn't need a defrost timer or defrost heater at all.  The customer in the discussion tells the expert this, but that doesn't keep the expert from getting confused and messing it up.  I chuckled a little bit and then found this discussion <link>;  once again, the expert changes his mind midstream and messes it up.

The last link is especially interesting because a couple of other repairmen get into it and try to straighten things out.  By the very bottom of the discussion, the actual possible problems finally show up.

So, what was wrong with mine?  The door needed to be adjusted.  Yep, the door has sagged a little bit over the years and was far enough down that it didn't close a switch at the top that turns on the fan for the evaporator.  See, if the fan isn't on, the evaporator goes into freezing range and the temperature sensor I mentioned above, shuts off the compressor to prevent ice forming and breaking the evaporator coil.  I drug out the tools and modified the top hinge to be adjustable and reset the door.  Problem solved.

The reason I had to modify the hinge was because GE made provisions for raising and lowering the door, but not tilting it.  They even avoid the subject in their installation manual (yes, I read it) like a door will never, ever skew a little bit.  The experts out there will tell me that I should adjust the fridge to assure that it's square because that can cause the same problem; I did that first with a nice level, framing square, and all.  Nope, the door actually needed to be adjusted.

But the point of this is that after I found the two discussions listed above, I found several others where the expert just wouldn't listen to the customer, assumed he knew everything, and screwed it up.

I'll never, ever ask a question on one of those sites.

But, there were a few good things that came out of this fiasco.  I moved the Smart Switch back to the fridge (I had it on the freezer for a few days), and took a look at the power usage during the problem:


Notice the small spikes?  It seems the compressor was turning on for a very short period, then shutting off for a very long period.  This was because the evaporator coil got too cold (no airflow) and the thermal protection kicked in and shut the compressor off.  This is very different from the graph I blogged about earlier <link>.  Another thing good about this was that I improved the performance of the appliance:


Now, the compressor is on longer, but off longer as well.  I turned the temperature control down and allowed it to stabilize at 38 F and all seems to be well.  Of course, something else can crop up and cause problems, but it looks like I fixed it.

Edit:  It's been a little over a day since I fixed the door on the fridge and I thought someone out there might be interested in the result.  Yes I fixed it, and I made out like a bandit on this:


Ignoring the couple of bumps in the middle of the chart, notice how it has settled down to a regular rythm?  It's on for about 15 minutes and off for around an hour and a half.  The huge spike in the middle is when I had the door open and the compressor kicked on at the same time; 300 W for the lights and around 400 W for the spike of the compressor starting.  The other bump right next to it was where I finally finished up assembling the fridge.  I put the drawers back in that I had take out to check things and also the temperature controller cover.  

Net, over time I'll get a feel for the usage of the fridge and a simple check every once in a while will tell me if it's working OK, or if it needs some attention.  It would be cool to completely automate the operation of this device with a little computer and radio combination, but not now, I want to take a really close look at the freezer setting nearby.  Then I'll take a look at the chest freezer I have out in the garage.  These appliance have been blamed by many people for using too much power, but my experience is the exact opposite.  The pool is my biggest electrical usage followed by the water heater.  Those I have under control with load balancing to off-peak periods and solar assistance.  Next is the two A/C units I have;  if I left them to run on their own, they would far exceed everything else, but I control when they can run also to minimize their impact on my wallet.

So, you folk that are looking at one of those multi-hundred dollar fancy thermostats to control the temperature in your house, think again.  You won't get nearly as much information from them as I do with my set up, and you won't be able to control it as well.  Sure, you'll have to actually learn something instead of buying the hype they try to sell you, but what's wrong with that?

Smart grid my butt, smart house is the solution.

Sunday, February 23, 2014

Raspberry Pi, USB, and XBee

I have two XBees hooked to my Pi now, one to control the network I've been building for a long time hooked to the serial port, and the other acting as a controller for my Iris Smart Switch hooked into a USB port.  I like the Smart Switch, so I'll be using more of them and that meant I had to figure out a better way to hook two XBees to the Pi and make it work.

I really didn't want to get into the mess of trying to get another standard serial port working because I eventually want LEDs for indicators of things that are important and maybe even a display that I can put words on.  Heck, I could decide to put voice on it at some point.  So, since the Pi comes with two USB ports, I decided to move both of the XBees to USB.  I started out with the USB explorer that I have to program the XBees:


I've had this board a long time and it just works.  Never had a problem with it, so I just plugged it into one of the USB ports on the Pi and changed my code to point to it.  It worked after a tiny bit of messing around.  Now I could control my own stuff and read the Smart Switch as well.  That proved that it could be done so I got two of these:


I chose this board so I could use a short USB cable to connect it, avoiding the problem of the bigger board above not fitting.  With these plugged into the Pi, I could still control the house and have all my GPIO pins available for whatever else I decide to do someday.  But, as usual, there was a problem.  See, USB is designed to be plugged and unplugged so you can change devices on the Pi, that unfortunately means that the address of the XBee will change each time you unplug it from the USB port.  So, one time I would have the smart switch on /dev/ttyUSB0 and the next time it would show up on /dev/ttyUSB1.  Makes coding for it more complex, and I really don't want complex right now.

It turns out that there is another way to connect to it.  Under the directory /dev is another directory 'serial'; looking in /dev/serial, I found two other directories by-id and by-path.  The by-id directory holds the device ids of the USB to serial chip and that doesn't change as you plug and unplug it.  So, I changed my code to define the serial port to this:

XBEEPORT = '/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_A901QLG3-if00-port0'

Yes, it's long and obscure, but that's what variables and comments are for.  It's also hard to type in, but that's what copy and paste are good for.  All in all, not a bad way to go as long as I don't reverse which little board I have the XBee plugged into.  Remember this little trick when your USB device seems to disappear.

So, now I have all the GPIO pins available; wonder what I'm going to plug into them.

Tuesday, February 11, 2014

Using the Iris Smart Switch to Measure Appliance Power

Since I finished the example of how to operate the Iris Smart Switch with a Raspberry Pi, I couldn't wait to hook it to something and do something real with it.  I decided to measure the power usage of my refrigerator.  First, about the refrigerator:  It's a refrigerator only, it doesn't have a freezer section; I have a separate freezer in the kitchen also.  There's a reason for this besides showing off.  Out here in the sticks where a trip to the grocery store is a major undertaking, we don't go very often.  That means plenty of food storage; I have a pantry, an upright freezer and separate refrigerator in the house and a chest freezer in the garage.  Yes, I can go about two weeks before I even have to go for bread and milk.  This makes things like a shopping list that is up to date a valuable skill.  I keep it up to date and every time I'm forced into town, it goes with me.  I don't want to waste a trip.

Anyway, I put together some code and read the switch, recorded the readings in my database and updated Xively with the data.  Then I suck the accumulated data from Xively and graph it to see what the appliance is doing.  I didn't implement the ability to turn the switch on and off since who wants to shut their refrigerator off??  Here's a days worth of data on the refrigerator:



Notice how it's only on for a short amount of time and that, other than the spike when the compressor starts, it doesn't use much power?  It's on roughly 15 minutes, and off for about 30 minutes in repeating cycles.  Here is an expansion of a portion of it:


The large spike on the first 'hump' is where the compressor kicks on.  It takes a lot of power to get the compressor going, but it drops off rapidly.  The granularity of the readings is only a minute, and if I was graphing every few seconds, you would see spikes on all the humps.  However, this is an accurate representation of what is happening overall.  Notice that the 550W spike is short and that the current draw decreases over time.  The average usage is probably around 110W or so for the period where the compressor is on.  That's the amount of energy used by some incandescent bulbs !  Let's see what a door open does to the chart:



Notice that the power usage jumps (the hump on the right) to 300W when the door is open?  It's pretty sobering that the power used by the lights when the door is open is actually more than the power used to keep the darn thing cold.  Now see why they say to close the door to save energy?  They just didn't tell us that the power loss was due to the bulbs, not the fact that we're warming up the fridge.  I'll probably get arguments from the energy nerds out there, but for my appliance, this is true.  Or, at least it is right now; I plan on getting LED bulbs to take care of that silly problem in the next couple of weeks.

Of course, there's the compressor kicking on right after I closed the door because I left the door open for a couple of minutes and it had to recover the temperature.  That's why the hump for the lights is on the far right, I actually went over and opened the door to get this reading.

All in all though, the fridge is not a huge power consumer.  It's less than running a 100W bulb all the time.  Actually, it's probably close to a 40W bulb since it's off twice as long as it's on in any given period.

Let's review.  I hacked into the Lowe's smart switch using an arduino (it's what I had to play with)<link>, ported the code to python so I could run it on a Raspberry Pi <link>, and then hooked it up to an appliance and learned that it was actually doing a good job.  Actually better than I expected.  Was it worth it?  Of course it was.  Now folks out there can take my example and measure their own appliances to compare them to their own expectations and needs.

Soon, I'll get another Smart Switch and put it in the circuit for the freezer and see how much power it uses.  I expect it to be higher than the fridge because it has the same set of lights, a bigger compressor and the circuitry for auto defrost.  Auto defrost is a real power monger since it actually warms up some areas of the appliance and drains the water out to be evaporated away.  I also want to see how often it cycles since it could happen that the compressor on the freezer and the compressor on the fridge could overlap their cycles.  That would cause a spike in power usage for the overlap period, which in turn, could cause my demand usage to rise.  That's the kind of thing I want to keep track of.  Demand billing is a painful thing because a simple mistake in power usage could double your monthly power bill.  I have a long discussion about my experiences here <link>.

Now, go out and get your own system of monitors running.  Take control of your house.

Addendum: As anyone can tell, I'm all about measuring things around the house to keep it under control, Power, time, temperature, runtime, etc are awesome tools to get your bills under control. But I also have calipers, micrometers, rulers and that kind of thing.

But with a smartphone, a whole new realm opens up. Take a look here for some stuff that I never even knew existed until I started looking for it.

https://joyofandroid.com/best-measuring-apps-for-android/

And that's just the beginning.

Monday, February 10, 2014

Raspberry Pi and the Lowe's Iris Smart Switch

So, I got the Lowe's Iris Smart Switch working pretty well for the Arduino <link>.  The problem is that it isn't where I want the software to run.  I have a Raspberry Pi controlling the house and this software should go there.  As I mentioned, I don't have a spare Pi right now, so I worked up the software for the Arduino with the full intention of moving it to the Pi as soon as it worked and I had time to mess with it.

Well, I decided the cool thing to do was to port the test software directly to the Pi in python and run it there.  I approached this with a little trepidation; taking things from an Arduino to another platform and language can drive you nuts.  First, I already have an XBee attached to the Pi; it uses the (only) serial port on the little device, so I got a Sparkfun XBee explorer <link> and plugged it into the USB port.  Fully expecting to have to jump through a bunch of hoops to get it to work, I did a simple 'cat /dev/ttyUSB0' command and actually got output to the screen on the very first try!

Sure it was garbage and didn't mean much, but I got output that corresponded to pushing the button on the switch.  Step one was done.  Next I put together a little code using the python XBee library to catch a packet and see what happened.  Right off the bat, I got this printed on the console of the Pi:

{'profile': '\xc2\x16', 'source_addr': '+\xd1', 'dest_endpoint': '\x02', 'rf_data': '\t\x00\x81T\x00', 'source_endpoint': '\x02', 'options': '\x01', 'source_addr_long': '\x00\ro\x00\x027\xb2Z', 'cluster': '\x00\xef', 'id': 'rx_explicit'}

Holy Cow, the library already had support for the ZigBee specific messages!  Notice that the fields have names already, and the ones that it took me so long to figure out are already there.  This means I can jump right in and start taking the messages from the switch apart.  It worked like a charm; there were some understanding problems in that the XBee library returns the data as character strings inside a dictionary of the various fields in a message, but these can be overcome once you catch on to what is happening.  Once I could decode the messages and print the power values and state of the switch, I implemented the commands from the Arduino code and they worked quite well.  So, here are the same capabilities that I presented for the Arduino implemented on the Raspberry Pi:

The Python Script
#! /usr/bin/python
# This is the an implementation of controlling the Lowe's Iris Smart
# Switch.  It will join with a switch and allow you to control the switch
#
#  Only ONE switch though.  This implementation is a direct port of the 
# work I did for an Arduino and illustrates what needs to be done for the 
# basic operation of the switch.  If you want more than one switch, you can
# adapt this code, or use the ideas in it to make your own control software.
#
# Have fun

from xbee import ZigBee 
from apscheduler.scheduler import Scheduler
import logging
import datetime
import time
import serial
import sys
import shlex


#-------------------------------------------------
# the database where I'm storing stuff
DATABASE='/home/pi/database/desert-home'

# on the Raspberry Pi the serial port is ttyAMA0
XBEEPORT = '/dev/ttyUSB0'
XBEEBAUD_RATE = 9600

# 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

switchLongAddr = '12'
switchShortAddr = '12'

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

#------------ XBee Stuff -------------------------
# Open serial port for use by the XBee
ser = serial.Serial(XBEEPORT, XBEEBAUD_RATE)

# this is a call back function.  When a message
# comes in this function will get the data
def messageReceived(data):
#   print 'gotta packet' 
#   print data
    # This is a test program, so use global variables and
    # save the addresses so they can be used later
    global switchLongAddr
    global switchShortAddr
    switchLongAddr = data['source_addr_long'] 
    switchShortAddr = data['source_addr']
    clusterId = (ord(data['cluster'][0])*256) + ord(data['cluster'][1])
    print 'Cluster ID:', hex(clusterId),
    if (clusterId == 0x13):
        # This is the device announce message.
        # due to timing problems with the switch itself, I don't 
        # respond to this message, I save the response for later after the
        # Match Descriptor request comes in.  You'll see it down below.
        # if you want to see the data that came in with this message, just
        # uncomment the 'print data' comment up above
        print 'Device Announce Message'
    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):
        # Match Descriptor Request; this is the point where I finally
        # respond to the switch.  Several messages are sent to cause the 
        # switch to join with the controller at a network level and to cause
        # it to regard this controller as valid.
        #
        # First the Active Endpoint Request
        payload1 = '\x00\x00'
        zb.send('tx_explicit',
            dest_addr_long = switchLongAddr,
            dest_addr = switchShortAddr,
            src_endpoint = '\x00',
            dest_endpoint = '\x00',
            cluster = '\x00\x05',
            profile = '\x00\x00',
            data = payload1
        )
        print 'sent Active Endpoint'
        # Now the Match Descriptor Response
        payload2 = '\x00\x00\x00\x00\x01\x02'
        zb.send('tx_explicit',
            dest_addr_long = switchLongAddr,
            dest_addr = switchShortAddr,
            src_endpoint = '\x00',
            dest_endpoint = '\x00',
            cluster = '\x80\x06',
            profile = '\x00\x00',
            data = payload2
        )
        print 'Sent Match Descriptor'
        # Now there are two messages directed at the hardware
        # code (rather than the network code.  The switch has to 
        # receive both of these to stay joined.
        payload3 = '\x11\x01\x01'
        zb.send('tx_explicit',
            dest_addr_long = switchLongAddr,
            dest_addr = switchShortAddr,
            src_endpoint = '\x00',
            dest_endpoint = '\x02',
            cluster = '\x00\xf6',
            profile = '\xc2\x16',
            data = payload2
        )
        payload4 = '\x19\x01\xfa\x00\x01'
        zb.send('tx_explicit',
            dest_addr_long = switchLongAddr,
            dest_addr = switchShortAddr,
            src_endpoint = '\x00',
            dest_endpoint = '\x02',
            cluster = '\x00\xf0',
            profile = '\xc2\x16',
            data = payload4
        )
        print 'Sent hardware join messages'

    elif (clusterId == 0xef):
        clusterCmd = ord(data['rf_data'][2])
        if (clusterCmd == 0x81):
            print 'Instantaneous Power',
            print ord(data['rf_data'][3]) + (ord(data['rf_data'][4]) * 256)
        elif (clusterCmd == 0x82):
            print "Minute Stats:",
            print 'Usage, ',
            usage = (ord(data['rf_data'][3]) +
                (ord(data['rf_data'][4]) * 256) +
                (ord(data['rf_data'][5]) * 256 * 256) +
                (ord(data['rf_data'][6]) * 256 * 256 * 256) )
            print usage, 'Watt Seconds ',
            print 'Up Time,',
            upTime = (ord(data['rf_data'][7]) +
                (ord(data['rf_data'][8]) * 256) +
                (ord(data['rf_data'][9]) * 256 * 256) +
                (ord(data['rf_data'][10]) * 256 * 256 * 256) )
            print upTime, 'Seconds'
    elif (clusterId == 0xf0):
        clusterCmd = ord(data['rf_data'][2])
        print "Cluster Cmd:", hex(clusterCmd),
        if (clusterCmd == 0xfb):
            print "Temperature ??"
        else:
            print "Unimplemented"
    elif (clusterId == 0xf6):
        clusterCmd = ord(data['rf_data'][2])
        if (clusterCmd == 0xfd):
            print "RSSI value:", ord(data['rf_data'][3])
        elif (clusterCmd == 0xfe):
            print "Version Information"
        else:
            print data['rf_data']
    elif (clusterId == 0xee):
        clusterCmd = ord(data['rf_data'][2])
        if (clusterCmd == 0x80):
            print "Switch is:",
            if (ord(data['rf_data'][3]) & 0x01):
                print "ON"
            else:
                print "OFF"
    else:
        print "Unimplemented Cluster ID", hex(clusterId)
        print

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
        )
    
#------------------If you want to schedule something to happen -----
#scheditem = Scheduler()
#scheditem.start()

#scheditem.add_interval_job(something, seconds=sometime)

#-----------------------------------------------------------------

# Create XBee library API object, which spawns a new thread
zb = ZigBee(ser, callback=messageReceived)

print "started at ", time.strftime("%A, %B, %d at %H:%M:%S")
print "Enter a number from 0 through 8 to send a command"
while True:
    try:
        time.sleep(0.001)
        str1 = raw_input("")
        # Turn Switch Off
        if(str1[0] == '0'):
            print 'Turn switch off'
            databytes1 = '\x01'
            databytesOff = '\x00\x01'
            sendSwitch(switchLongAddr, switchShortAddr, '\x00', '\x02', '\x00\xee', '\xc2\x16', '\x01', databytes1)
            sendSwitch(switchLongAddr, switchShortAddr, '\x00', '\x02', '\x00\xee', '\xc2\x16', '\x02', databytesOff)
        # Turn Switch On
        if(str1[0] == '1'):
            print 'Turn switch on'
            databytes1 = '\x01'
            databytesOn = '\x01\x01'
            sendSwitch(switchLongAddr, switchShortAddr, '\x00', '\x02', '\x00\xee', '\xc2\x16', '\x01', databytes1)
            sendSwitch(switchLongAddr, switchShortAddr, '\x00', '\x02', '\x00\xee', '\xc2\x16', '\x02', databytesOn)
        # this goes down to the test routine for further hacking
        elif (str1[0] == '2'):
            #testCommand()
            print 'Not Implemented'
        # This will get the Version Data, it's a combination of data and text
        elif (str1[0] == '3'):
            print 'Version Data'
            databytes = '\x00\x01'
            sendSwitch(switchLongAddr, switchShortAddr, '\x00', '\x02', '\x00\xf6', '\xc2\x16', '\xfc', databytes)
        # This command causes a message return holding the state of the switch
        elif (str1[0] == '4'):
            print 'Switch Status'
            databytes = '\x01'
            sendSwitch(switchLongAddr, switchShortAddr, '\x00', '\x02', '\x00\xee', '\xc2\x16', '\x01', databytes)
        # restore normal mode after one of the mode changess that follow
        elif (str1[0] == '5'):
            print 'Restore Normal Mode'
            databytes = '\x00\x01'
            sendSwitch(switchLongAddr, switchShortAddr, '\x00', '\x02', '\x00\xf0', '\xc2\x16', '\xfa', databytes)
        # range test - periodic double blink, no control, sends RSSI, no remote control
        # remote control works
        elif (str1[0] == '6'):
            print 'Range Test'
            databytes = '\x01\x01'
            sendSwitch(switchLongAddr, switchShortAddr, '\x00', '\x02', '\x00\xf0', '\xc2\x16', '\xfa', databytes)
        # locked mode - switch can't be controlled locally, no periodic data
        elif (str1[0] == '7'):
            print 'Locked Mode'
            databytes = '\x02\x01'
            sendSwitch(switchLongAddr, switchShortAddr, '\x00', '\x02', '\x00\xf0', '\xc2\x16', '\xfa', databytes)
        # Silent mode, no periodic data, but switch is controllable locally
        elif (str1[0] == '8'):
            print 'Silent Mode'
            databytes = '\x03\x01'
            sendSwitch(switchLongAddr, switchShortAddr, '\x00', '\x02', '\x00\xf0', '\xc2\x16', '\xfa', databytes)
#       else:
#           print 'Unknown Command'
    except IndexError:
        print "empty line"
    except KeyboardInterrupt:
        print "Keyboard interrupt"
        break
    except NameError as e:
        print "NameError:",
        print e.message.split("'")[1]
    except:
        print "Unexpected error:", sys.exc_info()[0]
        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()

Just like the Arduino code, this will allow a switch to join and then it will constantly update based on messages from the switch.  Here's some sample output from a run of this code:

Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xf0 Cluster Cmd: 0xfb Temperature ??
Cluster ID: 0xef Minute Stats: Usage,  60285 Watt Seconds  Up Time, 1200 Seconds
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xf0 Cluster Cmd: 0xfb Temperature ??
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xf0 Cluster Cmd: 0xfb Temperature ??
Cluster ID: 0xef Minute Stats: Usage,  65266 Watt Seconds  Up Time, 1260 Seconds
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xf0 Cluster Cmd: 0xfb Temperature ??
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xef Instantaneous Power 84
Cluster ID: 0xf0 Cluster Cmd: 0xfb Temperature ??
Cluster ID: 0xef Minute Stats: Usage,  70250 Watt Seconds  Up Time, 1320 Seconds
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xf0 Cluster Cmd: 0xfb Temperature ??
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xf0 Cluster Cmd: 0xfb Temperature ??
Cluster ID: 0xef Minute Stats: Usage,  75230 Watt Seconds  Up Time, 1380 Seconds
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xf0 Cluster Cmd: 0xfb Temperature ??
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xf0 Cluster Cmd: 0xfb Temperature ??
Cluster ID: 0xef Minute Stats: Usage,  80213 Watt Seconds  Up Time, 1440 Seconds
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xf0 Cluster Cmd: 0xfb Temperature ??
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xf0 Cluster Cmd: 0xfb Temperature ??
Cluster ID: 0xef Minute Stats: Usage,  85194 Watt Seconds  Up Time, 1500 Seconds
Cluster ID: 0xef Instantaneous Power 84
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xf0 Cluster Cmd: 0xfb Temperature ??
Cluster ID: 0xef Instantaneous Power 83
Cluster ID: 0xef Instantaneous Power 83

I had a little light hooked up to it that has two 40W incandescent bulbs in it so there was something to show.

Now I have the basics of reading the switch, and all I have to do now is hook it up with the rest of the software in the House Controller.  Then I can place these things wherever I want either, control of the power, or a measurement of how much power is being used.  Very nice little switch; I couldn't have built one for the price off the shelf at Lowe's.  And most importantly to me, I have control of it, not some cloud server or control device that I have to rely on a corporation's whim to change to fit my needs.

Have fun.

Thursday, February 6, 2014

Arduino and the Iris Zigbee switch, part 3

I've been working with the Iris Smart Switch for three weeks or so and I believe I have enough hacked out of it to make a useful addition to my home automation.  As I mentioned before, it controls an appliance as well as measuring the power usage of the device plugged into it.  Below is the code I ended up with in testing the device.  It's pretty verbose in the output as well as having a ton of comments to help the next person that wants to dig into this switch.

There are eight selections that can be chosen by typing a number into the arduino IDE input line and pressing send:

0 - turn the switch off
1 - turn the switch on
2 - special command routine
3 - get version data
4 - get current switch state (on, off)
5 - reset switch to normal mode
6 - range test
7 - local lock mode
8 - silent Mode

The special command routine is where you can try various commands to the switch.  Simply add code to your requirements and have at it. I used this selection to find various commands.  Range test returns the RSSI value for the switch and can be useful to tell if you have an RF path back to the controller.  Local lock mode disables the button on the switch, it can still be remote controlled, in this mode it doesn't return the periodic power data.  Silent Mode allows local control with the button as well as remote control, but the periodic data is not returned.

There's probably commands and operational characteristics to be found, but I think this is the critical set to put the switch into use.  My next project will be to actually use the darn thing to do something useful.

The Arduino Sketch
/**
This is an examination of Zigbee device communication using an XBee
and an Iris Smart Switch from Lowe's
*/
 
#include <XBee.h>
#include <SoftwareSerial.h>
#include <Time.h>
#include <TimeAlarms.h>

XBee xbee = XBee();
XBeeResponse response = XBeeResponse();
// create reusable response objects for responses we expect to handle 
ZBExpRxResponse rx = ZBExpRxResponse();

// Define NewSoftSerial TX/RX pins
// Connect Arduino pin 2 to Tx and 3 to Rx of the XBee
// I know this sounds backwards, but remember that output
// from the Arduino is input to the Xbee
#define ssRX 2
#define ssTX 3
SoftwareSerial nss(ssRX, ssTX);

XBeeAddress64 switchLongAddress;
uint16_t switchShortAddress;
uint16_t payload[50];
uint16_t myFrameId=1;

void setup() {  
  // start serial
  Serial.begin(9600);
  // and the software serial port
  nss.begin(9600);
  // now that they are started, hook the XBee into 
  // Software Serial
  xbee.setSerial(nss);
  // I think this is the only line actually left over
  // from Andrew's original example
  setTime(0,0,0,1,1,14);  // just so alarms work well, I don't really need the time.
  Serial.println("started");
}

void loop() {
    // doing the read without a timer makes it non-blocking, so
    // you can do other stuff in loop() as well.  Things like
    // looking at the console for something to turn the switch on
    // or off (see waaay down below)
    xbee.readPacket();
    // so the read above will set the available up to 
    // work when you check it.
    if (xbee.getResponse().isAvailable()) {
      // got something
//      Serial.println();
//      Serial.print("Frame Type is ");
      // Andrew called the XBee frame type ApiId, it's the first byte
      // of the frame specific data in the packet.
//      Serial.println(xbee.getResponse().getApiId(), HEX);
      //
      // All ZigBee device interaction is handled by the two XBee message type
      // ZB_EXPLICIT_RX_RESPONSE (ZigBee Explicit Rx Indicator Type 91)
      // ZB_EXPLICIT_TX_REQUEST (Explicit Addressing ZigBee Command Frame Type 11)
      // This test code only uses these and the Transmit Status message
      //
      if (xbee.getResponse().getApiId() == ZB_EXPLICIT_RX_RESPONSE) {
        // now that you know it's a Zigbee receive packet
        // fill in the values
        xbee.getResponse().getZBExpRxResponse(rx);
        
        // get the 64 bit address out of the incoming packet so you know 
        // which device it came from
//        Serial.print("Got a Zigbee explicit packet from: ");
        XBeeAddress64 senderLongAddress = rx.getRemoteAddress64();
//        print32Bits(senderLongAddress.getMsb());
//        Serial.print(" ");
//        print32Bits(senderLongAddress.getLsb());
        
        // this is how to get the sender's
        // 16 bit address and show it
        uint16_t senderShortAddress = rx.getRemoteAddress16();
//        Serial.print(" (");
//        print16Bits(senderShortAddress);
//        Serial.println(")");
        
        // for right now, since I'm only working with one switch
        // save the addresses globally for the entire test module
        switchLongAddress = rx.getRemoteAddress64();
        switchShortAddress = rx.getRemoteAddress16();

        //Serial.print("checksum is 0x");
        //Serial.println(rx.getChecksum(), HEX);
        
        // this is the frame length
        //Serial.print("frame data length is ");
        int frameDataLength = rx.getFrameDataLength();
        //Serial.println(frameDataLength, DEC);
        
        uint8_t* frameData = rx.getFrameData();
        // display everything after first 10 bytes
        // this is the Zigbee data after the XBee supplied addresses
//        Serial.println("Zigbee Specific Data from Device: ");
//        for (int i = 10; i < frameDataLength; i++) {
//          print8Bits(frameData[i]);
//          Serial.print(" ");
//        }
//        Serial.println();
        // get the source endpoint
//        Serial.print("Source Endpoint: ");
//        print8Bits(rx.getSrcEndpoint());
//        Serial.println();
        // byte 1 is the destination endpoint
//        Serial.print("Destination Endpoint: ");
//        print8Bits(rx.getDestEndpoint());
//        Serial.println();
        // bytes 2 and 3 are the cluster id
        // a cluster id of 0x13 is the device announce message
//        Serial.print("Cluster ID: ");
        uint16_t clusterId = (rx.getClusterId());
        print16Bits(clusterId);
        Serial.print(": ");
        // bytes 4 and 5 are the profile id
//        Serial.print("Profile ID: ");
//        print16Bits(rx.getProfileId());
//        Serial.println();
//        // byte 6 is the receive options
//        Serial.print("Receive Options: ");
//        print8Bits(rx.getRxOptions());
//        Serial.println();
//        Serial.print("Length of RF Data: ");
//        Serial.print(rx.getRFDataLength());
//        Serial.println();
//        Serial.print("RF Data Received: ");
//        for(int i=0; i < rx.getRFDataLength(); i++){
//            print8Bits(rx.getRFData()[i]);
//            Serial.print(' ');
//        }
//        Serial.println();
        //
        // I have the message and it's from a ZigBee device
        // so I have to deal with things like cluster ID, Profile ID
        // and the other strangely named fields that these devices use
        // for information and control
        //
        if (clusterId == 0x13){
          Serial.println("*** Device Announce Message");
          // In the announce message:
          // the next bytes are a 16 bit address and a 64 bit address (10) bytes
          // that are sent 'little endian' which means backwards such
          // that the most significant byte is last.
          // then the capabilities byte of the actual device, but
          // we don't need some of them because the XBee does most of the work 
          // for us.
          //
          // so save the long and short addresses
          switchLongAddress = rx.getRemoteAddress64();
          switchShortAddress = rx.getRemoteAddress16();
          // the data carried by the Device Announce Zigbee messaage is 18 bytes over
          // 2 for src & dest endpoints, 4 for cluster and profile ID, 
          // receive options 1, sequence number 1, short address 2, 
          // long address 8 ... after that is the data specific to 
          // this Zigbee message
//          Serial.print("Sequence Number: ");
//          print8Bits(rx.getRFData()[0]);
//          Serial.println();
//          Serial.print("Device Capabilities: ");
//          print8Bits(rx.getRFData()[11]);
//          Serial.println();
        }
        if (clusterId == 0x8005){ // Active endpoint response
          Serial.println("*** Active Endpoint Response");
          // You should get a transmit responnse packet back from the
          // XBee first, this will tell you the other end received 
          // something.
          // Then, an Active Endpoint Response from the end device
          // which will be Source Endpoint 0, Dest Endpoint 0,
          // Cluster ID 8005, Profile 0
          // it will have a payload, but the format returned by the 
          // Iris switch doesn't match the specifications.
          //
          // Also, I tried responding to this message directly after
          // its receipt, but that didn't work.  When I moved the 
          // response to follow the receipt of the Match Descriptor
          // Request, it started working.  So look below for where I 
          // send the response
        }
        if (clusterId == 0x0006){ // Match descriptor request
          Serial.println("*** Match Descriptor Request");
          // This is where I send the Active Endpoint Request 
          // which is endpoint 0x00, profile (0), cluster 0x0005
          uint8_t payload1[] = {0,0};
          ZBExpCommand tx = ZBExpCommand(switchLongAddress,
            switchShortAddress,
            0,    //src endpoint
            0,    //dest endpoint
            0x0005,    //cluster ID
            0x0000, //profile ID
            0,    //broadcast radius
            0x00,    //option
            payload1, //payload
            sizeof(payload1),    //payload length
            myFrameId++);   // frame ID
          xbee.send(tx);
//          Serial.println();
          //sendSwitch(0, 0, 0x0005, 0x0000, 0, 0, 0);

          Serial.print("sent active endpoint request ");
          //
          // So, send the next message, Match Descriptor Response,
          // cluster ID 0x8006, profile 0x0000, src and dest endpoints
          // 0x0000; there's also a payload byte
          //
          // {00.02} gave clicks
          uint8_t payload2[] = {0x00,0x00,0x00,0x00,0x01,02};
          tx = ZBExpCommand(switchLongAddress,
            switchShortAddress,
            0,    //src endpoint
            0,    //dest endpoint
            0x8006,    //cluster ID
            0x0000, //profile ID
            0,    //broadcast radius
            0x00,    //option
            payload2, //payload
            sizeof(payload2),    //payload length
            myFrameId++);   // frame ID
          xbee.send(tx);
//          Serial.println();
          Serial.print("sent Match Descriptor Response frame ID: ");
          Serial.println(myFrameId-1);
            
          //
          // Odd hardware message #1.  The next two messages are related 
          // to control of the hardware.  The Iris device won't stay joined with
          // the coordinator without both of these messages
          //
          uint8_t payload3[] = {0x11,0x01,0x01};
          tx = ZBExpCommand(switchLongAddress,
            switchShortAddress,
            0,    //src endpoint
            2,    //dest endpoint
            0x00f6,    //cluster ID
            0xc216, //profile ID
            0,    //broadcast radius
            0x00,    //option
            payload3, //payload
            sizeof(payload3),    //payload length
            myFrameId++);   // frame ID
            xbee.send(tx);
//            Serial.println();
            Serial.print("sent funny hardware message #1 frame ID: ");
            Serial.println(myFrameId-1);
            //
            // Odd hardware message #2
            //
            uint8_t payload4[] = {0x19,0x01,0xfa,0x00,0x01};
            tx = ZBExpCommand(switchLongAddress,
              switchShortAddress,
              0,    //src endpoint
              2,    //dest endpoint
              0x00f0,    //cluster ID
              0xc216, //profile ID
              0,    //broadcast radius
              0x00,    //option
              payload4, //payload
              sizeof(payload4),    //payload length
              myFrameId++);   // frame ID
              xbee.send(tx);
//              Serial.println();
              Serial.print("sent funny hardware message #2 frame ID: ");
              Serial.println(myFrameId-1);
            
        }
        else if (clusterId == 0xf6){
          // This is The Range Test command response.
          Serial.print("Cluster Cmd: ");
          Serial.print(rx.getRFData()[2],HEX);
          Serial.print(" ");
          Serial.print("*** Cluster ID 0xf6 ");
          if (rx.getRFData()[2] == 0xfd){
            Serial.print("RSSI value: ");
            print8Bits(rx.getRFData()[3]);
            Serial.print(" ");
            print8Bits(rx.getRFData()[4]);
            Serial.print(" ");
            Serial.print((int8_t)rx.getRFData()[3]);
            Serial.println();
          }
          else if (rx.getRFData()[2] == 0xfe){
            Serial.println("Version information");
            // bytes 0 - 2 are the packet overhead
            // This can be decoded to give the data from the switch,
            // but frankly, I didn't know what I would do with it 
            // once decoded, so I just skip it.
          }
          // This is to catch anything that may pop up in testing
          else{
            Serial.print(rx.getRFData()[2],HEX);
            Serial.print(" ");
            for(int i=0; i < rx.getRFDataLength(); i++){
              print8Bits(rx.getRFData()[i]);
              Serial.print(' ');
            }
            Serial.println();
          }
        }
        if (clusterId == 0x00f0){
//          Serial.println("Most likely a temperature reading; useless");
//          for(int i=0; i < rx.getRFDataLength(); i++){
//              print8Bits(rx.getRFData()[i]);
//              Serial.print(' ');
//          }
//          Serial.println();
          Serial.print("Cluster Cmd: ");
          Serial.print(rx.getRFData()[2],HEX);
          Serial.print(" ");
          uint16_t count = (uint8_t)rx.getRFData()[5] + 
              ((uint8_t)rx.getRFData()[6] << 8);
          Serial.print("Count: ");
          Serial.print(count);
          Serial.print(" ");
          Serial.print(rx.getRFData()[12]);
          Serial.print(" ");
          Serial.print(rx.getRFData()[13]);
          Serial.print(" ");
          uint16_t temp =  (uint8_t)rx.getRFData()[12] + 
              ((uint8_t)rx.getRFData()[13] << 8);
          Serial.println(temp);
//          temp = (temp / 1000) * 9 / 5 + 32;
//          Serial.println(temp);

        }
        if (clusterId == 0x00ef){
          //
          // This is a power report, there are two kinds, instant and summary
          //
          Serial.print("Cluster Cmd: ");
          Serial.print(rx.getRFData()[2],HEX);
          Serial.print(" ");
          Serial.print("*** Power Data, ");
          // The first byte is what Digi calls 'Frame Control'
          // The second is 'Transaction Sequence Number'
          // The third is 'Command ID'
          if (rx.getRFData()[2] == 0x81){
            // this is the place where instant power is sent
            // but it's sent 'little endian' meaning backwards
            int power = rx.getRFData()[3] + (rx.getRFData()[4] << 8);
            Serial.print("Instantaneous Power is: ");
            Serial.println(power);
          }
          else if (rx.getRFData()[2] == 0x82){
            unsigned long minuteStat = (uint32_t)rx.getRFData()[3] + 
              ((uint32_t)rx.getRFData()[4] << 8) + 
              ((uint32_t)rx.getRFData()[5] << 16) + 
              ((uint32_t)rx.getRFData()[6] << 24);
            unsigned long uptime = (uint32_t)rx.getRFData()[7] + 
              ((uint32_t)rx.getRFData()[8] << 8) + 
              ((uint32_t)rx.getRFData()[9] << 16) + 
              ((uint32_t)rx.getRFData()[10] << 24);
            int resetInd = rx.getRFData()[11];
            Serial.print("Minute Stat: ");
            Serial.print(minuteStat);
            Serial.print(" watt seconds, Uptime: ");
            Serial.print(uptime);
            Serial.print(" seconds, Reset Ind: ");
            Serial.println(resetInd);
          }
        }
        if (clusterId == 0x00ee){
          //
          // This is where the current status of the switch is reported
          //
          // If the 'cluster command' is 80, then it's a report, there
          // are other cluster commands, but they are controls to change
          // the switch.  I'm only checking the low order bit of the first
          // byte; I don't know what the other bits are yet.
          if (rx.getRFData()[2] == 0x80){
            Serial.print("Cluster Cmd: ");
            Serial.print(rx.getRFData()[2],HEX);
            Serial.print(" ");
            if (rx.getRFData()[3] & 0x01)
              Serial.println("Light switched on");
            else
              Serial.println("Light switched off");
          }
        }
      }
      else if (xbee.getResponse().getApiId() == ZB_TX_STATUS_RESPONSE) {
        ZBTxStatusResponse txStatus;
        xbee.getResponse().getZBTxStatusResponse(txStatus);
        Serial.print("Status Response: ");
        Serial.println(txStatus.getDeliveryStatus(), HEX);
        Serial.print("To Frame ID: ");
        Serial.println(txStatus.getFrameId());
      }
      else {
        Serial.print("Got frame type: ");
        Serial.println(xbee.getResponse().getApiId(), HEX);
      }
    }
    else if (xbee.getResponse().isError()) {
      // some kind of error happened, I put the stars in so
      // it could easily be found
      Serial.print("************************************* error code:");
      Serial.println(xbee.getResponse().getErrorCode(),DEC);
    }
    else {
      // I hate else statements that don't have some kind
      // ending.  This is where you handle other things
    }
    if (Serial.available() > 0) {
      char incomingByte;
      
      incomingByte = Serial.read();
      Serial.print("Selection: ");
      Serial.println(atoi(&incomingByte), DEC);
      // This message set will turn the light off
      if (atoi(&incomingByte) == 0){
        uint8_t payload1[] = {0x01}; //
        uint8_t payloadOff[] = {0x00,0x01};
        sendSwitch(0x00, 0x02, 0x00ee, 0xc216, 0x01, payload1, sizeof(payload1));
        sendSwitch(0x00, 0x02, 0x00ee, 0xc216, 0x02, payloadOff, sizeof(payloadOff));
      }
      // This pair of messages turns the light on
      else if (atoi(&incomingByte) == 1){
        uint8_t payload1[] = {0x01}; //
        uint8_t payloadOn[] = {0x01,0x01};
        sendSwitch(0x00, 0x02, 0x00ee, 0xc216, 0x01, payload1, sizeof(payload1));
        sendSwitch(0x00, 0x02, 0x00ee, 0xc216, 0x02, payloadOn, sizeof(payloadOn));
      }
      // this goes down to the test routine for further hacking
      else if (atoi(&incomingByte) == 2){
        testCommand();
      }
      // This will get the Version Data, it's a combination of data and text
      else if (atoi(&incomingByte) == 3){
        uint8_t data[] = {0x00, 0x01};
        sendSwitch(0x00, 0x02, 0x00f6, 0xc216, 0xfc, data, sizeof(data));
      }
      // This command causes a message return holding the state of the switch
      else if (atoi(&incomingByte) == 4){
        uint8_t data[] = {0x01};
        sendSwitch(0x00, 0x02, 0x00ee, 0xc216, 0x01, data, sizeof(data));
      }
      // restore normal mode after one of the mode changess that follow
      else if (atoi(&incomingByte) == 5){ 
        uint8_t databytes[] = {0x00, 0x01};
        sendSwitch(0, 0x02, 0x00f0, 0xc216, 0xfa, databytes, sizeof(databytes));
      }
      // range test - periodic double blink, no control, sends RSSI, no remote control
      // remote control works
      else if (atoi(&incomingByte) == 6){ 
        uint8_t databytes[] = {0x01, 0x01};
        sendSwitch(0, 0x02, 0x00f0, 0xc216, 0xfa, databytes, sizeof(databytes));
      }
      // locked mode - switch can't be controlled locally, no periodic data
      else if (atoi(&incomingByte) == 7){ 
        uint8_t databytes[] = {0x02, 0x01};
        sendSwitch(0, 0x02, 0x00f0, 0xc216, 0xfa, databytes, sizeof(databytes));
      }
      // Silent mode, no periodic data, but switch is controllable locally
      else if (atoi(&incomingByte) == 8){ 
        uint8_t databytes[] = {0x03, 0x01};
        sendSwitch(0, 0x02, 0x00f0, 0xc216, 0xfa, databytes, sizeof(databytes));
      }
    }
    Alarm.delay(0); // Just for the alarm routines

}

uint8_t testValue = 0x00;

void testCommand(){
  Serial.println("testing command");
  return;
  Serial.print("Trying value: ");
  Serial.println(testValue,HEX);
  uint8_t databytes[] = {};
  sendSwitch(0, 0xf0, 0x0b7d, 0xc216, testValue++, databytes, sizeof(databytes));
  if (testValue != 0xff)
    Alarm.timerOnce(1,testCommand); // try it again in a second
}

/*
  Because it got so cumbersome trying the various clusters for various commands,
  I created this send routine to make things a little easier and less prone to 
  typing mistakes.  It also made the code to implement the various commands I discovered
  easier to read.
*/
void sendSwitch(uint8_t sEndpoint, uint8_t dEndpoint, uint16_t clusterId,
        uint16_t profileId, uint8_t clusterCmd, uint8_t *databytes, int datalen){
          
  uint8_t payload [10];
  ZBExpCommand tx;
//  Serial.println("Sending command");
  //
  // The payload in a ZigBee Command starts with a frame control field
  // then a sequence number, cluster command, then databytes specific to
  // the cluster command, so we have to build it up in stages
  // 
  // first the frame control and sequence number
  payload[0] = 0x11;
  payload[1] = 0;
  payload[2] = clusterCmd;
  for (int i=0; i < datalen; i++){
    payload[i + 3] = databytes[i];
  }
  int payloadLen = 3 + datalen;
  // OK, now we have the ZigBee cluster specific piece constructed and ready to send
  
  tx = ZBExpCommand(switchLongAddress,
    switchShortAddress,
    sEndpoint,    //src endpoint
    dEndpoint,    //dest endpoint
    clusterId,    //cluster ID
    profileId, //profile ID
    0,    //broadcast radius
    0x00,    //option
    payload, //payload
    payloadLen,    //payload length
    myFrameId++);   // frame ID
    
    xbee.send(tx);
    Serial.print("sent command: ");
    Serial.print(payload[2], HEX);
    Serial.print(" frame ID: ");
    Serial.println(myFrameId-1);
}

/*-------------------------------------------------------*/
// these routines are just to print the data with
// leading zeros and allow formatting such that it 
// will be easy to read.
void print32Bits(uint32_t dw){
  print16Bits(dw >> 16);
  print16Bits(dw & 0xFFFF);
}

void print16Bits(uint16_t w){
  print8Bits(w >> 8);
  print8Bits(w & 0x00FF);
}
  
void print8Bits(byte c){
  uint8_t nibble = (c >> 4);
  if (nibble <= 9)
    Serial.write(nibble + 0x30);
  else
    Serial.write(nibble + 0x37);
        
  nibble = (uint8_t) (c & 0x0F);
  if (nibble <= 9)
    Serial.write(nibble + 0x30);
  else
    Serial.write(nibble + 0x37);
}
Here's a chunk of the sample output:

00EF: Cluster Cmd: 81 *** Power Data, Instantaneous Power is: 0
00EF: Cluster Cmd: 81 *** Power Data, Instantaneous Power is: 0
00EF: Cluster Cmd: 81 *** Power Data, Instantaneous Power is: 0
00F0: Cluster Cmd: FB Count: 37753 213 159 40917
00EF: Cluster Cmd: 81 *** Power Data, Instantaneous Power is: 0
00EF: Cluster Cmd: 81 *** Power Data, Instantaneous Power is: 0
00EF: Cluster Cmd: 81 *** Power Data, Instantaneous Power is: 0
00F0: Cluster Cmd: FB Count: 37873 214 159 40918
00EF: Cluster Cmd: 82 *** Power Data, Minute Stat: 0 watt seconds, Uptime: 58620 seconds, Reset Ind: 0
00EF: Cluster Cmd: 81 *** Power Data, Instantaneous Power is: 0
00EF: Cluster Cmd: 81 *** Power Data, Instantaneous Power is: 0
00EF: Cluster Cmd: 81 *** Power Data, Instantaneous Power is: 0
00F0: Cluster Cmd: FB Count: 37993 214 167 42966
00EF: Cluster Cmd: 81 *** Power Data, Instantaneous Power is: 0
00EF: Cluster Cmd: 81 *** Power Data, Instantaneous Power is: 0
00EF: Cluster Cmd: 81 *** Power Data, Instantaneous Power is: 0
00F0: Cluster Cmd: FB Count: 38113 216 149 38360
00EF: Cluster Cmd: 82 *** Power Data, Minute Stat: 0 watt seconds, Uptime: 58680 seconds, Reset Ind: 0
00EF: Cluster Cmd: 81 *** Power Data, Instantaneous Power is: 0
00EF: Cluster Cmd: 81 *** Power Data, Instantaneous Power is: 0
00EF: Cluster Cmd: 81 *** Power Data, Instantaneous Power is: 0
00F0: Cluster Cmd: FB Count: 38233 216 146 37592
00EF: Cluster Cmd: 81 *** Power Data, Instantaneous Power is: 0
00EF: Cluster Cmd: 81 *** Power Data, Instantaneous Power is: 0
00EF: Cluster Cmd: 81 *** Power Data, Instantaneous Power is: 0
00F0: Cluster Cmd: FB Count: 38353 217 147 37849
00EF: Cluster Cmd: 82 *** Power Data, Minute Stat: 0 watt seconds, Uptime: 58740 seconds, Reset Ind: 0
00EF: Cluster Cmd: 81 *** Power Data, Instantaneous Power is: 0
00EF: Cluster Cmd: 81 *** Power Data, Instantaneous Power is: 0
00EF: Cluster Cmd: 81 *** Power Data, Instantaneous Power is: 0
00F0: Cluster Cmd: FB Count: 38473 216 150 38616
00EF: Cluster Cmd: 81 *** Power Data, Instantaneous Power is: 0
00EF: Cluster Cmd: 81 *** Power Data, Instantaneous Power is: 0
00EF: Cluster Cmd: 81 *** Power Data, Instantaneous Power is: 0
00F0: Cluster Cmd: FB Count: 38593 217 150 38617
00EF: Cluster Cmd: 82 *** Power Data, Minute Stat: 0 watt seconds, Uptime: 58800 seconds, Reset Ind: 0
00EF: Cluster Cmd: 81 *** Power Data, Instantaneous Power is: 0
00EF: Cluster Cmd: 81 *** Power Data, Instantaneous Power is: 0
I didn't have anything plugged into it at the time, but you an see what the general output is.

Have fun

Friday, January 24, 2014

Arduino and the Iris Zigbee switch, part 2

Edit: The last portion of this investigation is here <link>

I've had a few days to play with the Iris switch <link> and I like it.  However it doesn't play well with others.  In that I mean I tried to bring it up on the network with my other devices and managed to kill my network of XBees.  Yes, I was down with all my devices not talking to each other and I had to visit each device and fix it.  This is totally avoidable, I was an idiot.  Do not follow in my footsteps in this.

First there are some things to know.  As I pointed out in my last post <link> about this switch, it takes special settings on the XBee for it to work with an Iris switch:

ZigBee Stack Profile 2
Encryption Enable 1
Encryption Options 1
API Enable 2
API Output Mode 3
Some Encryption Key
XBee Modem XB24-ZB, ZIGBEE Coordinator API, Version 21A7 (or better)

This is radically different from what I have been using in the past.  First, the Zigbee Stack Profile is 2, which means use the ZigBee Pro stack; I normally use a 0 in that spot.  When I changed it to profile 2, it stopped passing all the messages from my existing XBees through to the serial port.  Naturally, I thought that I could simply change the other devices to use the same profile parameter and everything should be fine.  Not so.

It seems the API Output Mode has a similar effect.  When you change API Output mode to something other than 0, you lose all the messages from other XBees except the ones that are explicitly allowed.  The message: ZigBee Explicit Rx Indicator (frame id=91) comes through fine, but you lose the ZigBee Receive Packet (frame id=90) which is what you normally get.  That means my remote XBees were happily sending messages and the receiver didn't pass them on to the serial port.  Like I said, I was an idiot because the setting clearly indicates either 'traditional' or 'explicit'.  I should have gotten a clue from this before I messed with the XBee network coordinator.

So, I couldn't see anything that was being sent by my other devices.  Naturally, I found this out AFTER I had reprogrammed my XBee coordinator.  My whole network was down.  The various XBee devices had saved their connection with the coordinator as it was set up before and I had to visit each one of them and make them drop the connection and establish a new one.  What a royal pain in the behind.

While I was doing that I decided there had to be a way to keep from having this happen in the future.  It turns out there is.  By adjusting some of the parameters, the remote XBees will sense that the coordinator is gone and automatically hunt for a new one.  This is a mixed blessing because if your coordinator dies, your remote devices will stop talking to each other.  I've had exactly zero problems with the XBee coordinator, so I decided to set the network up this way so that future experiments like this won't mean taking things apart to get to the XBee and forcing it to initialize the network.  The parameters are:

Network Watchdog Timeout 1
Channel Verification 1 (enabled)

The watchdog timeout causes the XBee to reestablish the network connection if it hasn't heard from the coordinator in three minutes.  Since my coordinator is sending the time repeatedly, all devices will hear from it often enough that the network shouldn't ever have a problem.  The Channel Verification being enabled means that any time the power fails the XBee will check to be sure there is an XBee coordinator out there when the power comes back on.  If it can't find the coordinator, it'll go looking for one.

These changes mean that if my coordinator dies, the network dies.  Fine, I can live with that.  It also means that there may be a very slight pause whenever the power comes back on from a failure.  When I tested that by unplugging a device, it took less than a second for it to find the coordinator and get back online.  So, now if I mess up the coordinator with some experiment, I won't have to go visit each darn XBee and make it talk to the new coordinator set up.  They'll do it by themselves after a three minute timeout, or I can unplug the power and make them do it right now.  Too bad I didn't discover this a couple of years ago.

Now, these changes may not be the right thing for you to do.  However, if you have an XBee in a hard to get to location, or setting way out there in the field hooked to a solar charger, it might be a good idea.  So, you understand your needs better than I can; make an informed decision.

All this information means something else: The Iris switch and my network won't work together.  Simple, I'll just set up a coordinator for the Iris Zigbee devices I may eventually have separate from the network I am using now and control them separately.  I should be able to interface an Arduino that has the code for Iris to anything I want to and go merrily on my way.  That may be the next project.

So, there was something good that came out of messing up my network of devices.  I learned a lot about modifying the network in general.  I also got to visit each of my XBees and clean out the spider webs and dead flies.  There was live scorpion in one of them, but since it's cool right now and the arthropod couldn't move very fast, I won.  I also decided to retire the separate Arduino for the Acid Pump and move the code and connection over to my Pool Controller.  Since I was already in the code for the Pool Controller, I hooked in the Septic Tank Float so I can get an alarm in the house if the tank has problems.  These were chores that I've been putting aside for months now.

How many people do you know that have a septic tank that can send them email?

Thursday, January 16, 2014

Arduino and the Iris Zigbee switch

A friend of mine is looking for a way to control a light remotely.  His problem, and mine also, is that these darn things are expensive and require a controller.  He specifically wanted something that used the Zigbee protocol because they are capable of being controlled by an XBee device.  He pointed me to the Iris line at Lowe's.  Being eaten up with curiosity, I went to Lowe's and looked at the devices.

They have an entire line of devices for lights, doors, garage, etc.  The problem I saw was that, once again, you're stuck with their controller that you can't change, their website that will probably go down at the least useful time (and has a number of times), and a monthly charge that they can raise any time they want to.  I even prowled through their terms of service (yuck) and it looks to me like they can use the data for anything they want to.  Also, they can change their terms of service at any time, leaving your data to their use.  I hate that crap.  Here we go, buy their stuff and then are subject to whatever they want to do over the years.  Why don't they just sell us the darn switches, publish a reasonable API that we can use, and let us live our lives outside their monthly charges and control?

What's needed here is for someone to figure out their switches, make them work with a little computer of some kind, and put how they did it on the web for the entire world to play with.  That'll show them.

Welcome to my work hooking an Arduino to an XBee and controlling an Iris light switch remotely.

Edit: But before you go and implement this, take a look at my second post on this experiment, there are some things to avoid <link>.

First, I chose their Iris Smart Plug <link> because it looks cool, can be tested without installing it into the wall, and measures the power going through it.  Does this sound like my perfect device or what?  I can use one of these to measure power anywhere in the house and have it report to my Raspberry Pi where I can forward it to a cloud server (or three, or four) for examination over time.  This little device is right down my alley.  Now, all I have to do is make it work ... right ?




I could have bought their controller and decoded the interaction with a sniffer and proceeded to duplicate it on my Pi, but there were some problems with this idea:  I don't have a spare Pi right now, the interaction is encrypted, I don't have a sniffer; what the heck am I going to do with this $30 paperweight?  Off to the web I go.

I only found one, yes one, place where anyone had any sort of success hacking this device.  Over at Jeelabs a contributor, CapnBry, had managed to make it work using code on a Windows PC <link>, but his description of how it worked read like classic Greek; totally out of my league.  Try as I might, there just wasn't anyplace else that turned up with something more comprehensible to me.  I had to just bite the bullet and start trying to talk to the switch.  After all, I've worked with XBees for years; how hard can it be?

At this point, you're probably thinking that this is just another post about how I tried to make it work and gave up because I just couldn't get enough information, didn't have the time, or the hardware didn't live up to expectations.  Well, not so; I have the working switch being remotely controlled by an Arduino setting right over there.  And yes, I'm going to tell you how I did it, and provide the code so you can repeat the experiment.  Hopefully, you'll expand on my efforts and let me know about it so we can all benefit and move along.

Like I said earlier, I wanted to put this on a Raspberry Pi, but I didn't have a spare one to experiment with, so I got an Arduino and XBee out of my parts bin, slapped them together, and programmed the XBee as a controller.  This is where I ran into my first obstacle.  How to set up the XBee?  First, you have to use a Series 2 XBee or better; none of this will work on a Series 1 XBee (now do you understand why I chose series 2)?  The notable items in the setup of the XBee are:

ZigBee Stack Profile 2
Encryption Enable 1
Encryption Options 1
API Enable 2
API Output Mode 3
XBee Modem XB24-ZB, ZIGBEE Coordinator API, Version 21A7 (or better)

And, Encryption Key set to something that you can remember.  You cannot read this register after you set it, so put in something you can't forget if you need it later.  Once set, I haven't needed it since, but you never know.

All the parameters can be set using XCTU and it is relatively easy to set up for this once you know what you're doing.  The other parameters can be matched to whatever you're used to using.  Don't think this came easily!  It took me almost a full day of messing around to get it working at all, so if you have trouble, double check everything.

Next, I wanted to use Andrew Rapp's XBee library for the Arduino, but he didn't put in support for the special messages needed to communicate with a ZigBee device.  Specifically there are two messages that are used (almost) exclusively for ZigBee communications: Explicit Addressing ZigBee Command Frame 0x11 and ZigBee Explicit Rx Indicator 0x91; these are not supported by Andrew's library.  However, the library is too nice to allow something like that to stop me, so I extended the library to support these two messages and added support to the ZBTxStatusResponse to be able to get the frame ID back (he missed that little thing).  These changes allowed me to use the library and all its features to speed up the hack.

OK, armed with a library specially modified to work with ZigBee devices and an XBee that should be able to talk to them, I put some code together to monitor traffic.  I immediately got traffic from the switch.  There was a series of messages that I couldn't understand and a lot of bytes to figure out what they meant.  Back to the link above where the guy stated that he had made it work.  The problem was with language.  He said things like, "You: (Endpoint=0x02, Profile=0xc216, Cluster=0x00f0) FrameControl=0x19 ClusterCmd=0xfa data=0x00 0x01"  What is an Endpoint?  A Profile?  These things were totally foreign and strange.  So I hunted down the ZigBee specification <link> and it was HUGE and filled with jargon specific to the protocol that made reading it an exercise in learning to speak ancient Sanskrit.  I also found an Endpoint document that talked about the interaction of devices during a process called 'Joining' <link>.  After reading a significant portion of these I started to understand.  None of the web sites I visited actually described what most of this stuff was, but it boils down to this:

There's a device that supports ZigBee as an end point.  It's things like light switches, door locks, devices that actually do something; these are called ZigBee Device Objects.  There's things that control these devices, they're called servers.  Each device has profiles, these profiles are code that are specific to the device.  Each profile has clusters; these clusters are where the code to do something is hooked.  So, you'll send a message to a device, profile something, cluster something, with some data about what you want to do.  Add to this the fact that each device has a 64 bit address, a 16 bit address, and needs special formatting to the message and the task becomes a bit daunting ... a whole lot daunting.


After prowling around documentation for hours I started to get a glimmer of what was going on and I finally found a key to these things that was way down in the XBee user's guide.  Round about page 122 the Digi folk talk about how to send and receive messages to a ZigBee device.  That was the key that got me going.  Now that I understood a little more than half of what the guy CapnBry was talking about, I started deconstructing the messages from the switch and looking at what was going on.


The switch sends a message announcing that it exists, then it sends another message that tells some stuff about what it can do.  These messages come after the hardware itself gets set up.  There are messages (we don't have to worry about) that take place just to get the XBee coordinator to recognize it.  Then the little device sends another message specifically from the hardware.  See, the ZigBee protocol is general purpose and has support for devices that haven't been invented yet, but the manufacturers ignore that and roll their own stuff under the 'Manufacturer Specific' provisions of the specification.  That means that even if you support the Zigbee protocol, it won't work with the various devices because they hide everything under the special provisions ... jerks.  So, you have to fiddle with things to get them to work.  The manufacturer AlertMe is notable for this tactic, and AlertMe manufactures the devices that Lowe's sells.  


So, these three messages come out of the switch and it's your responsibility to respond correctly to them and get the device to recognize your code as a valid controller; that's what meant by 'Joining'.  What happens is that the switch has a hardware 64 bit address and randomly chooses a 16 bit address to send the messages with.  If it doesn't get a proper response, it steps to another channel and tries the same thing again.  It does this for quite a while before it give up and just shuts down.  So you watch the messages, it stops a while and the messages start again.  Eventually it just give up all together.  Each time you see the set of three messages, it will have a different 16 bit address, so you have to save this address to respond to so that it will listen.  If you're too slow, it won't pay attention because it has already changed the address it pays attention to.


The sequence is documented in the code below, but basically, you wait for the first two messages to come in then respond to both of them.  Then, you interact with the device and it will join with your homemade controller.  From that point on, it will report the power usage every three seconds with a summary every minute.  There's other stuff that is sent by the switch, but I wasn't interested enough in it to bother decoding it; consider those items an exercise for the student.


Once you get it working, you'll find out how nice this switch is.  It monitors the power and reports it every three seconds or so.  It latches the state of the switch such that if the power fails, it comes back in the same state it was in when the power failed.  The little light on it doesn't follow properly, but that can be controlled with software.  It reports a state change back.  That means that if I walk over and push the button, it will send a message that the light has been changed.  You can ask the switch the state of the light and it will answer back to you.  So you can check to see if the outside lights were left on.  This switch is actually pretty nice.


But enough of the bragging.  Here's the code, but remember, this is an example of how to do it.  It isn't an example of coding style, proper formatting, or even the right way to do it.  It's the actual code I used to figure out how the switch works. It will compile using the Arduino IDE 1.0.5 and needs the special modifications to the XBee library I added to support the Zigbee specific messages.  Just let me know if you need the changes and I'll put them somewhere you can grab them.  It also needs the latest SoftwareSerial library because I use software pins to connect to the XBee and the console to monitor and send commands.


The Arduino Sketch
/* This is an examination of Zigbee device communication using an XBee Specifically using the Lowe's Iris switch. This device plugs into an outlet and has a plug on the front for your appliance. It controls the on/off of the appliance and measures the power usage as well. It's a lot like a remote control Kill-a-Watt. */ #include <XBee.h> #include <SoftwareSerial.h> XBee xbee = XBee(); XBeeResponse response = XBeeResponse(); // create response and command objects we expect to handle ZBExpRxResponse rx = ZBExpRxResponse(); XBeeAddress64 switchLongAddress; uint16_t switchShortAddress; uint16_t myFrameId=1; // for debugging, it's nice to know which messages is being handled // Define NewSoftSerial TX/RX pins // Connect Arduino pin 2 to Tx and 3 to Rx of the XBee // I know this sounds backwards, but remember that output // from the Arduino is input to the Xbee #define ssRX 2 #define ssTX 3 SoftwareSerial nss(ssRX, ssTX); void setup() { // start serial Serial.begin(9600); // and the software serial port nss.begin(9600); // now that they are started, hook the XBee into // Software Serial xbee.setSerial(nss); Serial.println("started"); } void loop() { // doing the read without a timer makes it non-blocking, so // you can do other stuff in loop() as well. Things like // looking at the console for something to turn the switch on // or off (see waaay down below) xbee.readPacket(); // so the read above will set the available up to // work when you check it. if (xbee.getResponse().isAvailable()) { // got something Serial.println(); Serial.print("Frame Type is "); // Andrew called the XBee frame type ApiId, it's the first byte // of the frame specific data in the packet. Serial.println(xbee.getResponse().getApiId(), HEX); // // All ZigBee device interaction is handled by the two XBee message type // ZB_EXPLICIT_RX_RESPONSE (ZigBee Explicit Rx Indicator Type 91) // ZB_EXPLICIT_TX_REQUEST (Explicit Addressing ZigBee Command Frame Type 11) // This test code only uses these and the Transmit Status message // if (xbee.getResponse().getApiId() == ZB_EXPLICIT_RX_RESPONSE) { // now that you know it's a Zigbee receive packet // fill in the values xbee.getResponse().getZBExpRxResponse(rx); // get the 64 bit address out of the incoming packet so you know // which device it came from Serial.print("Got a Zigbee explicit packet from: "); XBeeAddress64 senderLongAddress = rx.getRemoteAddress64(); print32Bits(senderLongAddress.getMsb()); Serial.print(" "); print32Bits(senderLongAddress.getLsb()); // this is how to get the sender's // 16 bit address and show it uint16_t senderShortAddress = rx.getRemoteAddress16(); Serial.print(" ("); print16Bits(senderShortAddress); Serial.println(")"); // for right now, since I'm only working with one switch // save the addresses globally for the entire test module switchLongAddress = rx.getRemoteAddress64(); switchShortAddress = rx.getRemoteAddress16(); //Serial.print("checksum is 0x"); //Serial.println(rx.getChecksum(), HEX); // this is the frame length //Serial.print("frame data length is "); int frameDataLength = rx.getFrameDataLength(); //Serial.println(frameDataLength, DEC); uint8_t* frameData = rx.getFrameData(); // display everything after first 10 bytes // this is the Zigbee data after the XBee supplied addresses Serial.println("Zigbee Specific Data from Device: "); for (int i = 10; i < frameDataLength; i++) { print8Bits(frameData[i]); Serial.print(" "); } Serial.println(); // get the source endpoint Serial.print("Source Endpoint: "); print8Bits(rx.getSrcEndpoint()); Serial.println(); // byte 1 is the destination endpoint Serial.print("Destination Endpoint: "); print8Bits(rx.getDestEndpoint()); Serial.println(); // bytes 2 and 3 are the cluster id // a cluster id of 0x13 is the device announce message Serial.print("Cluster ID: "); uint16_t clusterId = (rx.getClusterId()); print16Bits(clusterId); Serial.println(); // bytes 4 and 5 are the profile id Serial.print("Profile ID: "); print16Bits(rx.getProfileId()); Serial.println(); // byte 6 is the receive options Serial.print("Receive Options: "); print8Bits(rx.getRxOptions()); Serial.println(); Serial.print("Length of RF Data: "); Serial.print(rx.getRFDataLength()); Serial.println(); Serial.print("RF Data Received: "); for(int i=0; i < rx.getRFDataLength(); i++){ print8Bits(rx.getRFData()[i]); Serial.print(' '); } Serial.println(); // // I have the message and it's from a ZigBee device // so I have to deal with things like cluster ID, Profile ID // and the other strangely named fields that these devices use // for information and control // if (clusterId == 0x13){ Serial.println("*** Device Announce Message"); // In the announce message: // the next bytes are a 16 bit address and a 64 bit address (10) bytes // that are sent 'little endian' which means backwards such // that the most significant byte is last. // then the capabilities byte of the actual device, but // we don't need some of them because the XBee does most of the work // for us. // // so save the long and short addresses switchLongAddress = rx.getRemoteAddress64(); switchShortAddress = rx.getRemoteAddress16(); // the data carried by the Device Announce Zigbee messaage is 18 bytes over // 2 for src & dest endpoints, 4 for cluster and profile ID, // receive options 1, sequence number 1, short address 2, // long address 8 ... after that is the data specific to // this Zigbee message Serial.print("Sequence Number: "); print8Bits(rx.getRFData()[0]); Serial.println(); Serial.print("Device Capabilities: "); print8Bits(rx.getRFData()[11]); Serial.println(); } if (clusterId == 0x8005){ // Active endpoint response Serial.println("*** Active Endpoint Response"); // You should get a transmit responnse packet back from the // XBee first, this will tell you the other end received // something. // Then, an Active Endpoint Response from the end device // which will be Source Endpoint 0, Dest Endpoint 0, // Cluster ID 8005, Profile 0 // it will have a payload, but the format returned by the // Iris switch doesn't match the specifications. // // Also, I tried responding to this message directly after // its receipt, but that didn't work. When I moved the // response to follow the receipt of the Match Descriptor // Request, it started working. So look below for where I // send the response } if (clusterId == 0x0006){ // Match descriptor request Serial.println("*** Match Descriptor Request"); // This is where I send the Active Endpoint Request // which is endpoint 0x00, profile (0), cluster 0x0005 uint8_t payload1[] = {0,0}; ZBExpCommand tx = ZBExpCommand(switchLongAddress, switchShortAddress, 0, //src endpoint 0, //dest endpoint 0x0005, //cluster ID 0x0000, //profile ID 0, //broadcast radius 0x00, //option payload1, //payload sizeof(payload1), //payload length myFrameId++); // frame ID xbee.send(tx); Serial.println(); Serial.print("sent active endpoint request frame ID: "); Serial.println(myFrameId-1); // // So, send the next message, Match Descriptor Response, // cluster ID 0x8006, profile 0x0000, src and dest endpoints // 0x0000; there's also a payload byte // // {00.02} gave clicks uint8_t payload2[] = {0x00,0x00,0x00,0x00,0x01,02}; tx = ZBExpCommand(switchLongAddress, switchShortAddress, 0, //src endpoint 0, //dest endpoint 0x8006, //cluster ID 0x0000, //profile ID 0, //broadcast radius 0x00, //option payload2, //payload sizeof(payload2), //payload length myFrameId++); // frame ID xbee.send(tx); Serial.println(); Serial.print("sent Match Descriptor Response frame ID: "); Serial.println(myFrameId-1); // // Odd hardware message #1. The next two messages are related // to control of the hardware. The Iris device won't join with // the coordinator without both of these messages // uint8_t payload3[] = {0x11,0x01,0x01}; tx = ZBExpCommand(switchLongAddress, switchShortAddress, 0, //src endpoint 2, //dest endpoint 0x00f6, //cluster ID 0xc216, //profile ID 0, //broadcast radius 0x00, //option payload3, //payload sizeof(payload3), //payload length myFrameId++); // frame ID xbee.send(tx); Serial.println(); Serial.print("sent funny hardware message #1 frame ID: "); Serial.println(myFrameId-1); // // Odd hardware message #2 // uint8_t payload4[] = {0x19,0x01,0xfa,0x00,0x01}; tx = ZBExpCommand(switchLongAddress, switchShortAddress, 0, //src endpoint 2, //dest endpoint 0x00f0, //cluster ID 0xc216, //profile ID 0, //broadcast radius 0x00, //option payload4, //payload sizeof(payload4), //payload length myFrameId++); // frame ID xbee.send(tx); Serial.println(); Serial.print("sent funny hardware message #2 frame ID: "); Serial.println(myFrameId-1); } else if (clusterId == 0xf6){ // This is something specific to the Endpoint devices // and I haven't been able to find documentation on it // anywhere Serial.println("*** Cluster ID 0xf6"); } if (clusterId == 0x00ef){ // // This is a power report, there are two kinds, instant and summary // Serial.print("*** Power Data, "); // The first byte is what Digi calls 'Frame Control' // The second is 'Transaction Sequence Number' // The third is 'Command ID' if (rx.getRFData()[2] == 0x81){ // this is the place where instant power is sent // but it's sent 'little endian' meaning backwards int power = rx.getRFData()[3] + (rx.getRFData()[4] << 8); Serial.print("Instantaneous Power is: "); Serial.println(power); } else if (rx.getRFData()[2] == 0x82){ unsigned long minuteStat = (uint32_t)rx.getRFData()[3] + ((uint32_t)rx.getRFData()[4] << 8) + ((uint32_t)rx.getRFData()[5] << 16) + ((uint32_t)rx.getRFData()[6] << 24); unsigned long uptime = (uint32_t)rx.getRFData()[7] + ((uint32_t)rx.getRFData()[8] << 8) + ((uint32_t)rx.getRFData()[9] << 16) + ((uint32_t)rx.getRFData()[10] << 24); int resetInd = rx.getRFData()[11]; Serial.print("Minute Stat: "); Serial.print(minuteStat); Serial.print(" watt seconds, Uptime: "); Serial.print(uptime); Serial.print(" seconds, Reset Ind: "); Serial.println(resetInd); } } if (clusterId == 0x00ee){ // // This is where the current status of the switch is reported // // If the 'cluster command' is 80, then it's a report, there // are other cluster commands, but they are controls to change // the switch. I'm only checking the low order bit of the first // byte; I don't know what the other bits are yet. if (rx.getRFData()[2] == 0x80){ if (rx.getRFData()[3] & 0x01) Serial.println("Light switched on"); else Serial.println("Light switched off"); } } } else if (xbee.getResponse().getApiId() == ZB_TX_STATUS_RESPONSE) { ZBTxStatusResponse txStatus; xbee.getResponse().getZBTxStatusResponse(txStatus); Serial.print("Status Response: "); Serial.println(txStatus.getDeliveryStatus(), HEX); Serial.print("To Frame ID: "); Serial.println(txStatus.getFrameId()); } else { Serial.print("Got frame type: "); Serial.println(xbee.getResponse().getApiId(), HEX); } } else if (xbee.getResponse().isError()) { // some kind of error happened, I put the stars in so // it could easily be found Serial.print("************************************* error code:"); Serial.println(xbee.getResponse().getErrorCode(),DEC); } else { // I hate else statements that don't have some kind // ending. This is where you handle other things } if (Serial.available() > 0) { char incomingByte; incomingByte = Serial.read(); Serial.println(atoi(&incomingByte), DEC); if (atoi(&incomingByte) == 0){ // turn the light off lightSet(0); } else if (atoi(&incomingByte) == 1){ // turn the light on lightSet(1); } } } void lightSet(int val){ uint8_t payload1[] = {0x11,0x00,0x01,03}; uint8_t payload2[] = {0x11,0x00,0x02,0x00,0x01}; if (val==0){ Serial.println("Light Off"); } else { Serial.println("Light On"); payload2[3] = 0x01; } ZBExpCommand tx = ZBExpCommand(switchLongAddress, switchShortAddress, 0, //src endpoint 2, //dest endpoint 0x00ee, //cluster ID 0xc216, //profile ID 0, //broadcast radius 0x00, //option payload1, //payload sizeof(payload1), //payload length myFrameId++); // frame ID xbee.send(tx); Serial.println(); Serial.print("sent switch off 1 frame ID: "); Serial.println(myFrameId-1); tx = ZBExpCommand(switchLongAddress, switchShortAddress, 0, //src endpoint 2, //dest endpoint 0x00ee, //cluster ID 0xc216, //profile ID 0, //broadcast radius 0x00, //option payload2, //payload sizeof(payload2), //payload length myFrameId++); // frame ID xbee.send(tx); Serial.println(); Serial.print("sent switch off 2 frame ID: "); Serial.println(myFrameId-1); } // these routines are just to print the data with // leading zeros and allow formatting such that it // will be easy to read. void print32Bits(uint32_t dw){ print16Bits(dw >> 16); print16Bits(dw & 0xFFFF); } void print16Bits(uint16_t w){ print8Bits(w >> 8); print8Bits(w & 0x00FF); } void print8Bits(byte c){ uint8_t nibble = (c >> 4); if (nibble <= 9) Serial.write(nibble + 0x30); else Serial.write(nibble + 0x37); nibble = (uint8_t) (c & 0x0F); if (nibble <= 9) Serial.write(nibble + 0x30); else Serial.write(nibble + 0x37); }

Once you get it running you can turn the switch on by typing a 1 in the input line of the Arduino IDE terminal and clicking send.  A zero will turn the switch off.  That's all I really supported in this version, future work will obviously expand the capabilities.  Also, if you kill the sketch, wait until at least one message has been sent by the switch.  The code above needs the 16 and 64 bit address of the switch to work and I didn't put any provisions in to save it; it has to come from the switch.  Every message from the switch carries the addresses, so just wait for one to come in before trying to send something.  Since this is the very first version of this effort, the switch can sometimes fail to 'join'.  This isn't a problem, just let the two devices interact for about 20 seconds or so, unplug the switch and plug it back in.  It'll take off and work.

Edit: About an hour after I posted this I got a brainstorm and figured out how to get the switch to 'join' reliably.  Now, you can reset the switch by unplugging it, press the button to discharge any caps, plug it in, and then press the switch 8 times within about 8 seconds.  The switch will start all over in its interaction, and then start sending power readings.  When (notice I didn't say 'if') I get another one, I'll have to modify this code to support two devices, but one works fine.  The code box above has been updated to hold the latest.

Here's the output from the Arduino from first start up after joining.  I turn the switch on and off during the session.  Notice that the power usage is 83 (two little bulbs in a lamp) and that the switch is constantly sending its status over the network.  There's an extra piece of debugging in this; I print all the bytes sent to the XBee, so the lines that begin with the 7E are lines that are actually being sent to the switch.

started

Frame Type is 91
Got a Zigbee explicit packet from: 000D6F00 0237B25A (A6C4)
Source Endpoint: 02
Destination Endpoint: 02
Cluster ID: 00EF
Profile ID: C216
Length of RF Data: 5
RF Data Received: 09 00 81 53 00 
Power Data, Instantaneous Power is: 83

Frame Type is 91
Got a Zigbee explicit packet from: 000D6F00 0237B25A (A6C4)
Source Endpoint: 02
Destination Endpoint: 02
Cluster ID: 00F0
Profile ID: C216
Length of RF Data: 16
RF Data Received: 09 00 FB 1C 26 25 DA 03 4A 32 00 00 CB EA 01 00 
0
Light Off
7E 0 18 11 1 0 D 6F 0 2 37 B2 5A A6 C4 0 2 0 EE C2 16 0 0 11 0 1 3 E5 
sent switch off 1 frame ID: 1
7E 0 19 11 2 0 D 6F 0 2 37 B2 5A A6 C4 0 2 0 EE C2 16 0 0 11 0 2 0 1 E5 
sent switch off 2 frame ID: 2

Frame Type is 8B
Status Response: 0
To Frame ID: 2

Frame Type is 91
Got a Zigbee explicit packet from: 000D6F00 0237B25A (A6C4)
Source Endpoint: 02
Destination Endpoint: 02
Cluster ID: 00EE
Profile ID: C216
Length of RF Data: 5
RF Data Received: 09 70 80 06 E0 
Light switched off

Frame Type is 91
Got a Zigbee explicit packet from: 000D6F00 0237B25A (A6C4)
Source Endpoint: 02
Destination Endpoint: 02
Cluster ID: 00EF
Profile ID: C216
Length of RF Data: 12
RF Data Received: 09 00 82 A4 0D 00 00 90 F6 00 00 00 
Power Data, Minute Stat: 3492 watt seconds, Uptime: 63120 seconds, Reset Ind: 0

Frame Type is 91
Got a Zigbee explicit packet from: 000D6F00 0237B25A (A6C4)
Source Endpoint: 02
Destination Endpoint: 02
Cluster ID: 00EF
Profile ID: C216
Length of RF Data: 5
RF Data Received: 09 00 81 50 00 
Power Data, Instantaneous Power is: 80

Frame Type is 91
Got a Zigbee explicit packet from: 000D6F00 0237B25A (A6C4)
Source Endpoint: 02
Destination Endpoint: 02
Cluster ID: 00EF
Profile ID: C216
Length of RF Data: 5
RF Data Received: 09 00 81 00 00 
Power Data, Instantaneous Power is: 0
1
Light On
7E 0 18 11 3 0 D 6F 0 2 37 B2 5A A6 C4 0 2 0 EE C2 16 0 0 11 0 1 3 E3 
sent switch off 1 frame ID: 3
7E 0 19 11 4 0 D 6F 0 2 37 B2 5A A6 C4 0 2 0 EE C2 16 0 0 11 0 2 1 1 E2 
sent switch off 2 frame ID: 4

Frame Type is 8B
Status Response: 0
To Frame ID: 4

Frame Type is 91
Got a Zigbee explicit packet from: 000D6F00 0237B25A (A6C4)
Source Endpoint: 02
Destination Endpoint: 02
Cluster ID: 00EE
Profile ID: C216
Length of RF Data: 5
RF Data Received: 09 70 80 07 00 
Light switched on

Frame Type is 91
Got a Zigbee explicit packet from: 000D6F00 0237B25A (A6C4)
Source Endpoint: 02
Destination Endpoint: 02
Cluster ID: 00EF
Profile ID: C216
Length of RF Data: 5
RF Data Received: 09 00 81 53 00 
Power Data, Instantaneous Power is: 83

Frame Type is 91
Got a Zigbee explicit packet from: 000D6F00 0237B25A (A6C4)
Source Endpoint: 02
Destination Endpoint: 02
Cluster ID: 00F0
Profile ID: C216
Length of RF Data: 16
RF Data Received: 09 00 FB 1C 26 9D DA 03 4A 32 00 00 C6 F6 01 00 

Have fun.