Showing posts with label Zigbee. Show all posts
Showing posts with label Zigbee. Show all posts

Thursday, October 30, 2014

ZIgBee Protocol, XBee, but this time an Arduino !

A couple of days ago I posted how to get a switch using the ZigBee protocol running with an XBee <link>; I'm pretty proud of that, but there's lots of folk out there with an Arduino already.  What about them?  Since I love those little board, I ported the code over to it as well.  See, the protocol is huge and extensive, but once you know how to operate a device that uses the protocol, you don't need much code to keep doing it.  That makes the little Arduino a cool device for controlling a ZigBee switch.

This code is the rough equivalent of the code I posted previously for the Raspberry Pi, same selections, except I left out the one that interrogates the switch, and let it report when the switch joins with the Arduino.  So, go look at the explanation I've already done to see how it works and how to use it.  Here's the Arduino Sketch:

/**
This is an implementation of Zigbee device communication using an XBee
and a Centralite Smart Switch 4256050-ZHAC

dave  (www.desert-home.com)
*/
 
// This code will handle both a Uno and a Mega2560 by careful use of
// the defines.  I tried it on both of them, and the only problem is that
// SoftwareSerial sometimes loses characters because the input buffer
// is too small.  If you have this problem, see the SoftwareSerial 
// documentation to see how to change it.
#include <XBee.h>
//#include <SoftwareSerial.h>
#include <Time.h>
#include <TimeAlarms.h>

// create reusable objects for messages we expect to handle
// using them over and over saves memory instead of sucking it off the
// stack every time we need to send or receive a message by creating
// a new object
XBee xbee = XBee();
XBeeResponse response = XBeeResponse();
ZBExpRxResponse rx = ZBExpRxResponse();
ZBExpCommand tx;
XBeeAddress64 Broadcast = XBeeAddress64(0x00000000, 0x0000ffff);

// Define the hardware serial port for the XBee (mega board)
#define ssRX 2
#define ssTX 3
// Or 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
//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);
  // Or the hardware serial port
  Serial1.begin(9600);
  // now that they are started, hook the XBee into 
  // whichever one you chose
  //xbee.setSerial(nss);
  xbee.setSerial(Serial1);
  setTime(0,0,0,1,1,14);  // just so alarms work well, I don't really need the time.
  Serial.println("started");
}

boolean firstTime = true;

void loop() {
  // Since this test code doesn't have the switch address, I'll
  // send a message to get the routes to the devices on the network
  // All devices are supposed to respond to this, and even the
  // XBee we're hooked to will respond for us automatically
  // The second message in will be the switch we want to work
  // with.  Thus, giving us the address we need to do things with
  if (firstTime){
    Serial.println(F("Wait while I locate the device"));
  // First broadcast a route record request so when the switch responds
  // I can get the addresses out of it
    Serial.println(F("Sending Route Record Request"));
    uint8_t rrrPayload[] = {0x12,0x01};
    tx = ZBExpCommand(Broadcast, //This will be broadcast to all devices
      0xfffe,
      0,    //src endpoint
      0,    //dest endpoint
      0x0032,    //cluster ID
      0x0000, //profile ID
      0,    //broadcast radius
      0x00,    //option
      rrrPayload, //payload
      sizeof(rrrPayload),    //payload length
      0x00);   // frame ID
    xbee.send(tx);
  firstTime = false;
  }

  // First, go check the XBee, This is non-blocking, so 
  // if nothing is there, it will just return.  This also allows
  // any message to come in at any time so thing can happen 
  // automatically.
  handleXbee();
  // After checking the XBee for data, look at the serial port
  // This is non blocking also.
  handleSerial();
  // Now, update the timer and do it all over again.
  // This code tries to not wait for anything.  It keeps it
  // from hanging up unexpectedly.  This way we can implement a 
  // watchdog timer to take care of the occasional problem.
  Alarm.delay(0); // Just for the alarm routines
}

void handleSerial(){
  if (Serial.available() > 0) {
    char incomingByte;
    
    incomingByte = Serial.read();
    // Originally, I had a routine to send messages, but it tended to hide 
    // the way the messages were constructed from new folk.  I changed it
    // back to a verbose construction of each message sent to control the
    // switch so people could more easily understand what they needed to do
    if (isdigit(incomingByte)){
      Serial.print("Selection: ");
      int selection = atoi(&incomingByte);
      Serial.print(selection, DEC);
      switch(selection){
        case 0: { // switch off
          Serial.println(F(" Turn switch off"));
          // In these outgoing messages I set the transaction sequence
          // number to 0xaa so it could be easily seen if I was dumping
          // messages as they went out.
          uint8_t offPayload[] = {0x11,0xaa,0x00};
          tx = ZBExpCommand(switchLongAddress,
            switchShortAddress,
            0,    //src endpoint
            1,    //dest endpoint
            0x0006,    //cluster ID
            0x0104, //profile ID
            0,    //broadcast radius
            0x00,    //option
            offPayload, //payload
            sizeof(offPayload),    //payload length
            0x00);   // frame ID
            xbee.send(tx);
          break;
        }
        case 1: { // switch on
          Serial.println(F(" Turn switch on"));
          uint8_t onPayload[] = {0x11,0xaa,0x01};
          tx = ZBExpCommand(switchLongAddress,
            switchShortAddress,
            0,    //src endpoint
            1,    //dest endpoint
            0x0006,    //cluster ID
            0x0104, //profile ID
            0,    //broadcast radius
            0x00,    //option
            onPayload, //payload
            sizeof(onPayload),    //payload length
            0x00);   // frame ID
          xbee.send(tx);
          break;
        }
        case 2: { // switch toggle
          Serial.println(F(" Toggle switch"));
          uint8_t togglePayload[] = {0x11,0xaa,0x02};
          tx = ZBExpCommand(switchLongAddress,
            switchShortAddress,
            0,    //src endpoint
            1,    //dest endpoint
            0x0006,    //cluster ID
            0x0104, //profile ID
            0,    //broadcast radius
            0x00,    //option
            togglePayload, //payload
            sizeof(togglePayload),    //payload length
            0x00);   // frame ID
          xbee.send(tx);
          break;
        }
        case 3: {
          Serial.println(F(" Dim"));
          uint8_t dimPayload[] = {0x11,0xaa,0x00,25,0x32,0x00};
          tx = ZBExpCommand(switchLongAddress,
            switchShortAddress,
            0,    //src endpoint
            1,    //dest endpoint
            0x0008,    //cluster ID
            0x0104, //profile ID
            0,    //broadcast radius
            0x00,    //option
            dimPayload, //payload
            sizeof(dimPayload),    //payload length
            0x00);   // frame ID
          xbee.send(tx);
          break;
        }
        case 4: {
          Serial.println(F(" Bright"));
          uint8_t brightPayload[] = {0x11,0xaa,0x00,255,0x32,0x00};
          tx = ZBExpCommand(switchLongAddress,
            switchShortAddress,
            0,    //src endpoint
            1,    //dest endpoint
            0x0008,    //cluster ID
            0x0104, //profile ID
            0,    //broadcast radius
            0x00,    //option
            brightPayload, //payload
            sizeof(brightPayload),    //payload length
            0x00);   // frame ID
          xbee.send(tx);
          break;
        }
        case 5: {
          Serial.println(F(" Get State of Light "));
          uint8_t ssPayload[] = {0x00,0xaa,0x00,0x00,0x00};
          tx = ZBExpCommand(switchLongAddress,
            switchShortAddress,
            0,    //src endpoint
            1,    //dest endpoint
            0x0006,    //cluster ID
            0x0104, //profile ID
            0,    //broadcast radius
            0x00,    //option
            ssPayload, //payload
            sizeof(ssPayload),    //payload length
            0x00);   // frame ID
          xbee.send(tx);
          break;
        }
        
        default:     
          Serial.println(F(" Try again"));
          break;
      }
      // Now a short delay combined with a character read
      // to empty the input buffer.  The IDE developers removed
      // the input flush that used to work for this.
      while(Serial.available() > 0){
        char t = Serial.read();
        delay(25);
      }
    }
  }
}
void handleXbee(){
  // 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 
  xbee.readPacket();
  // 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.
    int frameType = xbee.getResponse().getApiId();
    //Serial.println(frameType, 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 (frameType == ZB_EXPLICIT_RX_RESPONSE) {
      // now that you know it's a Zigbee receive packet
      // fill in the values
      xbee.getResponse().getZBExpRxResponse(rx);
      int senderProfileId = rx.getProfileId();
      // For this code, I decided to switch based on the profile ID.
      // The interaction is based on profile 0, the general one and
      // profile 0x0104, the Home Automation profile
      //Serial.print(F(" Profile ID: "));
      //Serial.print(senderProfileId, HEX);
      
      // get the 64 bit address out of the incoming packet so you know 
      // which device it came from
      //Serial.print(" 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();

      uint8_t* frameData = rx.getFrameData();
      // We're working with a message specifically designed for the
      // ZigBee protocol, see the XBee documentation to get the layout
      // of the message.
      //
      // 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
      //
      // I grab the cluster id out of the message to make the code
      // below simpler.
      //Serial.print(F(" Cluster ID: "));
      uint16_t clusterId = (rx.getClusterId());
      //print16Bits(clusterId);
      //
      // Note that cluster IDs have different uses under different profiles
      //  First I'll deal with the general profile.
      if (senderProfileId == 0x0000){  // This is the general profile
        if (clusterId == 0x00){
          //Serial.println(F(" Basic Cluster"));
          pass();
        }
        else if (clusterId == 0x0006){ // Match Descriptor
          //Serial.println(F(" Match Descriptor"));
          /*************************************/
          // I don't actually need this message, but it comes in as soon as
          // a device is plugged in.  I answer it with a messsage that says I
          // have an input cluster 0x19, since that's what it's looking for.
          // Ignoring this message doesn't seem to hurt anything either.
          uint8_t mdPayload[] = {0xAA,0x00,0x00,0x00,0x01,0x19};
          mdPayload[2] = switchShortAddress & 0x00ff;
          mdPayload[3] = switchShortAddress >> 8;
          ZBExpCommand tx = ZBExpCommand(switchLongAddress,
            switchShortAddress,
            0,    //src endpoint
            0,    //dest endpoint
            0x8006,    //cluster ID
            0x0000, //profile ID
            0,    //broadcast radius
            0x00,    //option
            mdPayload, //payload
            sizeof(mdPayload),    //payload length
            0x00);   // frame ID
          xbee.send(tx);
          // if you unplug a device, and then plug it back in, it loses the 
          // configuration for reporting on/off changes.  So, send the configuration
          // to get the switch working the way I want it to after the match
          // descriptor message.  
          Serial.println (F("sending cluster command Configure Reporting "));
          uint8_t crPayload[] = {0x00,0xaa,0x06,0x00,0x00,0x00,0x10,0x00,0x00,0x00,0x40,0x00,0x00};
          tx = ZBExpCommand(switchLongAddress,
            switchShortAddress,
            0,    //src endpoint
            1,    //dest endpoint
            0x0006,    //cluster ID
            0x0104, //profile ID
            0,    //broadcast radius
            0x00,    //option
            crPayload, //payload
            sizeof(crPayload),    //payload length
            0x00);   // frame ID
          xbee.send(tx);
          
       }
        else if (clusterId == 0x0013){  //device announce message
          // any time a new device joins a network, it's supposed to send this
          // message to tell everyone its there.  Once you get this message,
          // you can interogate the new device to find out what it is, and 
          // what it can do.
          Serial.println(F(" Device Announce Message"));
          switchLongAddress = rx.getRemoteAddress64();
          switchShortAddress = rx.getRemoteAddress16();
          // Ok we saw the switch, now just for fun, get it to tell us
          // what profile it is using and some other stuff.
          // We'll send an Acttive Endpoint Request to do this
          Serial.println (F("sending Active Endpoint Request "));
          // The active endpoint request needs the short address of the device
          // in the payload.  Remember, it needs to be little endian (backwards)
          // The first byte in the payload is simply a number to identify the message
          // the response will have the same number in it.
          uint8_t aePayload[] = {0xAA,0x00,0x00};
          aePayload[1] = switchShortAddress & 0x00ff;
          aePayload[2] = switchShortAddress >> 8;
          ZBExpCommand tx = ZBExpCommand(switchLongAddress,
            switchShortAddress,
            0,    //src endpoint
            0,    //dest endpoint
            0x0005,    //cluster ID
            0x0000, //profile ID
            0,    //broadcast radius
            0x00,    //option
            aePayload, //payload
            sizeof(aePayload),    //payload length
            0xaa);   // frame ID
          xbee.send(tx);
        }
        else if (clusterId == 0x8004){
          Serial.println(F(" Simple Descriptor Response "));
          // Since I've been through this switch a few times, I already know
          // what to expect out of it.  This response is how you get the actual 
          // clusters that it has code for, and the profile ID that it supports.
          // Since this is a light switch, it will support profile 0x104 and have
          // clusters that support things like on/off and reporting.
          // The items of interest are in the rf_data payload, and this is one way
          // to get them out.
          unsigned char *data = rx.getRFData();  // first get a pointer to the data
          Serial.print(F(" Transaction ID: "));
          print16Bits(data[0]); // remember the number that starts the payload?
          Serial.println();
          Serial.print(F(" Endpoint Reported: "));
          print8Bits(data[5]);
          Serial.println();
          Serial.print(F(" Profile ID: "));
          print8Bits(data[7]);  // Profile ID is 2 bytes long little endian (backwards)
          print8Bits(data[6]);
          Serial.println();
          Serial.print(F(" Device ID: "));
          print8Bits(data[9]);  // Device ID is 2 bytes long little endian (backwards)
          print8Bits(data[8]);
          Serial.println();
          Serial.print(F(" Device Version: "));
          print8Bits(data[10]);  // Device ID is 1 byte long
          Serial.println();
          Serial.print(F(" Number of input clusters: "));
          print8Bits(data[11]);  // Input cluster count
          Serial.print(F(", Clusters: "));
          Serial.println();
          for (int i = 0; i < data[11]; i++){
            Serial.print(F("    "));
             print8Bits(data[i*2+13]); // some more of that little endian crap
             print8Bits(data[i*2+12]);
             Serial.println();
          }
          int outidx = 11 + 1 + 2*data[11];
          Serial.print(F(" Number of output clusters: "));
          print8Bits(data[outidx]);  // Input cluster count
          Serial.print(F(", Clusters: "));
          Serial.println();
          for (int i = 0; i < data[outidx]; i++){
            Serial.print(F("    "));
             print8Bits(data[i*2 + outidx + 2]); // some more of that little endian crap
             print8Bits(data[i*2 + outidx + 1]);
             Serial.println();
          }
          Serial.println (F("sending cluster command Configure Reporting "));
          // OK, for illustration purposes, this is enough to actually do something
          // First though, let's set up the switch so that it reports when it has 
          // changed states in the on/off cluster (cluster 0006).  This will require we
          // send a message to the on/off cluster with the "Configure Reporting" command
          // (0x06) with a bunch of parameters to specify things.
          uint8_t crPayload[] = {0x00,0xaa,0x06,0x00,0x00,0x00,0x10,0x00,0x00,0x00,0x40,0x00,0x00};
          ZBExpCommand tx = ZBExpCommand(switchLongAddress,
            switchShortAddress,
            0,    //src endpoint
            1,    //dest endpoint
            0x0006,    //cluster ID
            0x0104, //profile ID
            0,    //broadcast radius
            0x00,    //option
            crPayload, //payload
            sizeof(crPayload),    //payload length
            0x00);   // frame ID
          xbee.send(tx);

        } 
        else if (clusterId == 0x8005){
          Serial.println(F(" Active Endpoints Response"));
          // This message tells us which endpoint to use
          // when controlling the switch.  Since this is only a switch,
          // it will give us back one endpoint.  I should really have a loop
          // in here to handle multiple endpoints, but ...
          Serial.print(F("  Active Endpoint Count reported: "));
          Serial.println(rx.getRFData()[4]);
          Serial.print(F("  Active Endpoint: "));
          Serial.println(rx.getRFData()[5]);
          // Now we know that it has an endpoint, but we don't know what profile
          // the endpoint is under.  So, we send a Simple Descriptor Request to get
          // that.
          Serial.println (F("sending Simple Descriptor Request "));
          // The request needs the short address of the device
          // in the payload.  Remember, it needs to be little endian (backwards)
          // The first byte in the payload is simply a number to identify the message
          // the response will have the same number in it.  The last number is the
          // endpoint we got back in the Active Endpoint Response.  
          // Also note that we're still dealing with profile 0 here, we haven't gotten
          // to the device we actually want to play with yet.
          uint8_t sdPayload[] = {0xAA,0x00,0x00,01};
          sdPayload[1] = switchShortAddress & 0x00ff;
          sdPayload[2] = switchShortAddress >> 8;
          sdPayload[3] = rx.getRFData()[5];
          ZBExpCommand tx = ZBExpCommand(switchLongAddress,
            switchShortAddress,
            0,    //src endpoint
            0,    //dest endpoint
            0x0004,    //cluster ID
            0x0000, //profile ID
            0,    //broadcast radius
            0x00,    //option
            sdPayload, //payload
            sizeof(sdPayload),    //payload length
            0xaa);   // frame ID
          xbee.send(tx);
        }
        else if (clusterId == 0x8032){
          Serial.print(" Response from: ");
          print16Bits(senderShortAddress);
          Serial.println();
          if(switchShortAddress != 0x0000){
            Serial.print(F("Got switch address "));
            Serial.println(F("Ready"));
          }
        }
        else{
          Serial.print(F(" Haven't implemented this cluster yet: "));
          Serial.println(clusterId,HEX);
        }
      }
      else if(senderProfileId == 0x0104){   // This is the Home Automation profile
        // Since these are all ZCL (ZigBee Cluster Library) messages, I'll suck out
        // the cluster command, and payload so they can be used easily.
        //Serial.println();
        //Serial.print(" RF Data Received: ");
        //for(int i=0; i < rx.getRFDataLength(); i++){
            //print8Bits(rx.getRFData()[i]);
            //Serial.print(' ');
        //}
        //Serial.println();
        if (clusterId == 0x0000){
          //Serial.print(F(" Basic Cluster"));
          pass();
        }
        else if (clusterId == 0x0006){ // Switch on/off 
          // Serial.println(F(" Switch on/off"));
          // with the Centralite switch, we don't have to respond
          // A message to this cluster tells us that the switch changed state
          // However, if the response hasn't been configured, it will give back 
          // default response (cluster command 0b)
          // so let's dig in and see what's going on.
          //
          // The first two bytes of the rfdata are the ZCL header, the rest of
          // the data is a three field indicator of the attribute that changed
          // two bytes of attribute identifier, a byte of datatype, and some bytes
          // of the new value of the attribute.  Since this particular attribute is a 
          // boolean (on or off), there will only be one byte.  So
          if(rx.getRFData()[2] == 0x0b){ // default response (usually means error)
            Serial.println(F("  Default Response: "));
            Serial.print(F("  Command: "));
            print8Bits(rx.getRFData()[3]);  
            Serial.println();
            Serial.print(F("  Status: "));
            print8Bits(rx.getRFData()[4]);  
            Serial.println();
          }
          else if (rx.getRFData()[2] == 0x0a || rx.getRFData()[2] == 0x01){
             // This is what we really want to know
            Serial.print(F("Light "));
            // The very last byte is the status
            if (rx.getRFData()[rx.getRFDataLength()-1] == 0x01){
              Serial.println(F("On"));
            }
            else{
              Serial.println(F("Off"));
            }
          }
          else{  // for now, the ones above were the only clusters I needed.
            //Serial.println(F("  I don't know what this is"));
            pass();
          }
        }
        else if (clusterId == 0x0008){ // This is the switch level cluster
          // right now I don't do anything with it, but it's where
          // the switch lets you know about level changes
        }
        else{
          Serial.print(F(" Haven't implemented this cluster yet: "));
          Serial.println(clusterId,HEX);
        }
      }
    }
    else {
      if (frameType == 0xa1){
        //Serial.println(F(" Route Record Request"));
        pass();
      }
      else if (frameType == ZB_TX_STATUS_RESPONSE){
        //Serial.print(F(" Transmit Status Response"));
        pass();
      }
      else{
        Serial.print(F("Got frame type: "));
        Serial.print(frameType, HEX);
        Serial.println(F(" I didn't implement this frame type for this experiment"));
      }
    }
  }
  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 {
    // If you get here it only means you haven't collected enough bytes from
    // the XBee to compose a packet.
  }
}

/*-------------------------------------------------------*/
// null routine to avoid some syntax errors when debugging
void pass(){
    return;
}
// 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);
}

Remember, on an Arduino the XBee API mode must be set to 2.

Have fun with it.

Tuesday, October 28, 2014

OK, Back to the ZigBee protocol and XBees ... AGAIN

I managed to hack into the Iris Smart Switch from Lowe's and they've been working fine, but there's always been this nagging little annoyance bothering me.  The Alertme Switch that Lowe's sells is NOT ZigBee compliant no matter what they may tell you.  In hacking at it I pointed out a few things that were not according to spec (yes, I've actually read that massive spec related to home automation), and I've been wondering what it would be like to work with an actual ZigBee device.

A reader set me up with one of these:


This is a Centralite 4256050-ZHAC and has an impressive set of capabilities.  They claim that it will work with any controller that is compliant with the ZigBee HA (Home Automation) specification.  That sounds like a challenge to me.  If it is compliant, I should be able to figure out how to work it using an XBee; so away I went.

After almost six DAYS of poking messages at the switch, I was only a tiny bit further along than I was when I started.  This silly thing simply wouldn't join with the XBee so I could see anything it did.  Then I stumbled across a note that said it had a special key.  Key?  It needs a key?  OK, I can try this.  It started working.  I was able to send something and get an answer back; sure the answer was an error, but it was an answer.  Thus began my exploration of the ZigBee protocol in earnest; I was going to make this switch work.

Once again, this isn't one of those posts where I tried for weeks and finally gave up because there just wasn't enough information out there, the machine was too slow, or someone kept something secret; I made it work and will give you the code and XBee configuration to follow in my footsteps down below.  But first I want to talk about the ZigBee protocol and its relationship to XBees a bit.

First, this is an incredibly complex protocol and not for the faint of heart.  Just learning some of the jargon is daunting, much less trying to put it to use.  Sure, there are libraries out there, but have you looked at the prices of those things?  I simply can't afford to mess with them at that price.  Also, the libraries are as hard to understand as the protocol, AND it has the overhead of the library that has to be learned also.  I kept wondering if the XBee somehow could help with this.  Turns out the XBee really can do ZigBee, there just aren't may people that have tried.  Actually, I couldn't find anyone besides me that actually had.

There are lots of pictures and explanations out there about the ideas behind ZigBee, and some of them are even correct, but it was still hard for me to understand.  Let me give you some basics.  The protocol has profiles, these are collections of specs and suggestions for the operation of a system.  Things like Home Automation, Electric Lights (that Hue thingie), Smart Energy, and a device can support one or more of these things.  I'm interested in Home Automation, they call it HA, and that's where I concentrated.  Within this they separate data that you read or change and call them attributes.  These attributes are put within clusters.  Don't get confused, this isn't really that tough.

Within the HA profile, there are some defined clusters and they have numbers that identify them.  Let's take cluster 0x0006, the on-off cluster.  This will have an attribute, the state of the device, and it is numbered 0x0000 and has a datatype of boolean; it tells you if the switch is on or off.  To read this attribute you send a command to the cluster asking for it and the cluster returns the number identifier, datatype and value of the attribute.  See, clusters have commands to operate on them and attributes that you can use.

To tell if the switch is on, send a cluster command 0x00 (read attribute) to cluster 0x0006 (on/off) and the device will send back a command 0x01 (read attribute response) to cluster 0x0006 with the attribute identifier, datatype, value.  Cool.

In the message you send, you also specify the endpoint you want the reply to be sent to and the endpoint you are sending to.  What's an endpoint?  An endpoint is simply a collection of clusters.  On the centralite switch, it has endpoints 0, the general one, and 1, specific to this device.  The general endpoint is where stuff that you want to deal with of a general nature goes and endpoint 1 is where you send stuff that deals with turning the light on and off.

Thus, you have to worry about profiles, endpoints, clusters, clusters commands, and attributes.  Actually it's not that bad, it's just hard to ferret out of the thousands of pages of documentation.  But, you ask, how does the XBee help me?  The XBee eliminates about half of the documentation from being necessary for us to mess with.  It handles all the interactions to set up a network, keep track of device routing, radio initialization, that stuff.  It also gives us a simpler (not simple) message format to use so we don't have to worry about the six or seven layers of protocol, we work totally at the application level and just let it do the rest.  Heck, it even handles the encryption for us.

Combine an XBee to handle the low level link stuff and our own code to handle the application, and you have a reasonable way of controlling these switches that are on the market.  Let me show you the command above in python:

zb.send('tx_explicit',
 dest_addr_long = switchLongAddr,
 dest_addr = switchShortAddr,
 src_endpoint = '\x00',
 dest_endpoint = '\x01',
 cluster = '\x00\x06', # cluster I want to deal with
 profile = '\x01\x04', # home automation profile
 data = '\x00'+'\xaa'+'\x00'+'\x00'+'\x00'
)

There's the long address, it's 32 bits long and doesn't change, ever.  The short address, it's 16 bits long and changes every time the switch joins with the controller; yes, even after a power failure.  The source endpoint.  This is zero because I didn't want to deal with more than one in my code; all the responses come back to endpoint zero.  The destination endpoint which is one on this switch. The cluster id of six as I mentioned above.  The profile 0x0104 which is the number for the HA protocol. And, some data.  The data is one byte of control bits, a byte transaction sequence number that I set to 0xaa so it would be easy recognize, the cluster command 0x00, and the attribute id of 0x0000.  The reason it is shown as ascii characters is a characteristic of the python XBee library implementation.

This may be confusing at first, but trust me, it actually makes sense once you get into it a bit.

This message will send a response to profile id 0x104, endpoint 00, cluster 0x0006, with a payload of 0x00, 0x00, 0x10, 01 if the light is on.  The first two bytes are the attribute id, the next byte is the datatype (0x10 means boolean) and the last byte is 1, meaning the switch is closed.

Are you getting an idea of how this works?  Now, I can hear you asking, "How the heck do I find out these values?"  They're documented in the Cluster Specification document, and there are messages that will itemize the endpoints and clusters within them that the device supports.  So, you send a message to the device to get the endpoints, it tells you what they are, then for each endpoint you ask what the attributes are and it responds.  You look at this stuff, see what you need and use it.

Actually makes sense in a deranged computer scientist sort of way.  But, let's talk about the setup for an XBee specifically to support the Home Automation profile.  That's what I wanted, to be able to turn this switch on and off.  First, it's different from the setup used on the Iris switch so don't think about that, take this as new.

Software Zigbee API Coordinator
Zigbee Stack Profile  (ZS) 2
Encryption Enable  (EE) 1
Encryption Options (EO) 0
Encryption Key  (KY) 5a6967426565416c6c69616e63653039
Network Encryption Key (NK) 0
API Enable (AP) 1
API Output Mode (AO) 3

Yes, you have to use the key.  That part took me the first week of messing with this to find out.  Of course, now that I know what to look for, it would take me about a minute to get it, but that's how we learn.  The difference in the encryption setup is what prevents this switch and the Iris switch from working with the same controller.  You can't have it both ways at once.  If anyone thinks of a way around this, let me know.

Once you have the XBee setup like this you can take the Centralite switch, press the button and hold it, then plug it in the wall.  When the led turns on, let go of the switch and it will join with the XBee automatically.  Yes, that's all there is to joining.  The two devices take care of it themselves and all you have to do is discover the device and start using it.  This is very different from the Alertme devices where we have to mess around with special secret commands to get it to work.  This device actually complies with the specification.

In the code below, I send a message asking for route information and grab the switch's address out of the response.  Then, I send it a command to set up reporting for the light and just wait for someone to tell the code what to do with the light. The commands are:

0 - Turn the switch off
1 - Turn the switch on
2 - Toggle the switch
3 - Dim the switch
4 - Brighten the switch
5 - Tell me the status of the switch
6 - Send messages and print responses about the switch.

Yes, the switch is capable of dimming a light.  The last command goes through a portion of the Zigbee discovery process to find out which endpoints are supported and what clusters and attributes are in them.  It's voluminous, but it's the first time I was actually able to see what the various buzz words actually represented.  This is the kind of thing I did to conquer the way the switch works.

#! /usr/bin/python

'''
This is an examination of a REAL ZigBee device.  The CentraLite 4256050-ZHAC

It has an impressive array of capabilities that I don't delve into in depth in
this examination, but it responds properly to the various ZigBee commands and holds
the clusters necessary to control a switch.

Nice little device
'''

# This is the super secret home automation key that is needed to 
# implement the HA profile.
# KY parameter on XBee = 5a6967426565416c6c69616e63653039
# Have fun

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

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

'''
 This routine will print the data received so you can follow along if necessary
'''
def printData(data):
 for d in data:
  print d, ' : ',
  for e in data[d]:
   print "{0:02x}".format(ord(e)),
  if (d =='id'):
   print "({})".format(data[d]),
  print

def getAttributes(data, thisOne):
 ''' OK, now that I've listed the clusters, I'm going to see about 
 getting the attributes for one of them by sending a Discover
 attributes command.  This is not a ZDO command, it's a ZCL command.
 ZDO = ZigBee device object - the actual device
 ZCL = Zigbee cluster - the collection of routines to control it.
  
  frame control bits = 0b00 (this means a BINARY 00)
  manufacturer specific bit = 0, for normal, or one for manufacturer
  So, the frame control will be 0000000
  discover attributes command identifier = 0x0c
  
  then a zero to indicate the first attribute to be returned
  and a 0x0f to indicate the maximum number of attributes to 
  return.
 '''
 print "Sending Discover Attributes, Cluster:", repr(thisOne)
 zb.send('tx_explicit',
  dest_addr_long = data['source_addr_long'],
  dest_addr = data['source_addr'],
  src_endpoint = '\x00',
  dest_endpoint = '\x01',
  cluster = thisOne, # cluster I want to know about
  profile = '\x01\x04', # home automation profile
  # means: frame control 0, sequence number 0xaa, command 0c,
  # start at 0x0000 for a length of 0x0f
  data = '\x00' + '\xaa' + '\x0c'+ '\x00' + '\x00'+ '\x0f'
  )

# this is a call back function.  When a message
# comes in this function will get the data
def messageReceived(data):
 global switchLongAddr
 global switchShortAddr
 
 try:
  #print 'gotta packet',
  #printData(data)  # uncomment this to see the data returned
  
  # Since this is a test program, it will only support one switch
  # go get the long and short address out of the incoming packet
  # for more than one switch, this won't work
  switchLongAddr = data['source_addr_long']
  switchShortAddr = data['source_addr']
  
  if (data['id'] == 'rx_explicit'):
   #print "RF Explicit"
   #printData(data)
   clusterId = (ord(data['cluster'][0])*256) + ord(data['cluster'][1])
   print 'Cluster ID:', hex(clusterId),
   print "profile id:", repr(data['profile']),
   if (data['profile']=='\x01\x04'): # Home Automation Profile
    # This has to be handled differently than the general profile
    # each response if from a cluster that is actually doing something
    # so there are attributes and commands to think about.
    #
    # Since this is a ZCL message; which actually means this message is 
    # is supposed to use the ZigBee cluster library to actually do something
    # like turn on a light or check to see if it's on, the command way down
    # in the rf_data is important.  So, the commands may be repeated in
    # each cluster and do slightly different things
    #
    # I'm going to grab the cluster command out of the rf_data first so 
    # I don't have to code it into each cluster
    #print "take this apart"
    #print repr(data['rf_data'])
    if (data['rf_data'][0] == '\x08'): # was it successful?
     #should have a bit check to see if manufacturer data is here
     cCommand = data['rf_data'][2]
     print "Cluster command: ", hex(ord(cCommand))
    else:
     print "Cluster command failed"
     return
    # grab the payload data to make it easier to work with
    payload = data['rf_data'][3:] #from index 3 on is the payload for the command
    datatypes={'\x00':'no data',
       '\x10':'boolean',
       '\x18':'8 bit bitmap',
       '\x20':'unsigned 8 bit integer',
       '\x21':'unsigned 24 bit integer',
       '\x30':'8 bit enumeration',
       '\x42':'character string'}
    #print "Raw payload:",repr(payload)
    # handle these first commands in a general way
    if (cCommand == '\x0d'): # Discover Attributes
     # This tells you all the attributes for a particular cluster
     # and their datatypes
     print "Discover attributes response"
     if (payload[0] == '\x01'):
      print "All attributes returned"
     else:
      print "Didn't get all the attributes on one try"
     i = 1
     if (len(payload) == 1): # no actual attributes returned
      print "No attributes"
      return
     while (i < len(payload)-1):
      print "    Attribute = ", hex(ord(payload[i+1])) , hex(ord(payload[i])),
      try:
       print datatypes[payload[i+2]]
       i += 3
      except:
       print "I don't have an entry for datatype:", hex(ord(payload[i+2]))
       return
       
    if (clusterId == 0x0000): # Under HA this is the 'Basic' Cluster
     pass
    elif (clusterId == 0x0003): # 'identify' should make it flash a light or something 
     pass
    elif (clusterId == 0x0004): # 'Groups'
     pass
    elif (clusterId == 0x0005): # 'Scenes'  
     pass
    elif (clusterId == 0x0006): # 'On/Off' this is for switching or checking on and off  
     #print "inside cluster 6"
     if cCommand in ['\x0a','\x01']:
      # The very last byte tells me if the light is on.
      if (payload[-1] == '\x00'):
       print "Light is OFF"
      else:
       print "Light is ON"
     pass
    elif (clusterId == 0x0008): # 'Level'  
     pass
    else:
     print("Haven't implemented this yet")
   elif (data['profile']=='\x00\x00'): # The General Profile
    if (clusterId == 0x0000):
     print ("Network (16-bit) Address Request")
     #printData(data)
    elif (clusterId == 0x0008):
     # I couldn't find a definition for this 
     print("This was probably sent to the wrong profile")
    elif (clusterId == 0x0004):
     # Simple Descriptor Request, 
     print("Simple Descriptor Request")
     print("I don't respond to this")
     #printData(data)
    elif (clusterId == 0x0013):
     # This is the device announce message.
     print 'Device Announce Message'
     #printData(data)
     # This is a newly found device, so I'm going to tell it to 
     # report changes to the switch.  There are better ways of
     # doing this, but this is a test and demonstration
     print "sending 'configure reporting'"
     zb.send('tx_explicit',
      dest_addr_long = switchLongAddr,
      dest_addr = switchShortAddr,
      src_endpoint = '\x00',
      dest_endpoint = '\x01',
      cluster = '\x00\x06', # cluster I want to deal with
      profile = '\x01\x04', # home automation profile
      data = '\x00' + '\xaa' + '\x06' + '\x00' + '\x00' + '\x00' + '\x10' + '\x00' + '\x00' + '\x00' + '\x40' + '\x00' + '\x00'
     )
    elif (clusterId == 0x8000):
     print("Network (16-bit) Address Response")
     #printData(data)
    elif (clusterId == 0x8032):
     print "Route Record Response"
    elif (clusterId == 0x8038):
     print("Management Network Update Request");
    elif (clusterId == 0x8005):
     # this is the Active Endpoint Response This message tells you
     # what the device can do
     print 'Active Endpoint Response'
     printData(data)
     if (ord(data['rf_data'][1]) == 0): # this means success
      print "Active Endpoint reported back is: {0:02x}".format(ord(data['rf_data'][5]))
     print("Now trying simple descriptor request on endpoint 01")
     zb.send('tx_explicit',
      dest_addr_long = data['source_addr_long'],
      dest_addr = data['source_addr'],
      src_endpoint = '\x00',
      dest_endpoint = '\x00', # This has to go to endpoint 0 !
      cluster = '\x00\x04', #simple descriptor request'
      profile = '\x00\x00',
      data = '\x13' + data['source_addr'][1] + data['source_addr'][0] + '\x01'
     )
    elif (clusterId == 0x8004):
     print "simple descriptor response"
     try:
      clustersFound = []
      r = data['rf_data']
      if (ord(r[1]) == 0): # means success
       #take apart the simple descriptor returned
       endpoint, profileId, deviceId, version, inCount = \
        unpack('<BHHBB',r[5:12])
       print "    endpoint reported is: {0:02x}".format(endpoint)
       print "    profile id:  {0:04x}".format(profileId)
       print "    device id: {0:04x}".format(deviceId)
       print "    device version: {0:02x}".format(version)
       print "    input cluster count: {0:02x}".format(inCount)
       position = 12
       # input cluster list (16 bit words)
       for x in range (0,inCount):
        thisOne, = unpack("<H",r[position : position+2])
        clustersFound.append(r[position+1] + r[position])
        position += 2
        print "        input cluster {0:04x}".format(thisOne)
       outCount, = unpack("<B",r[position])
       position += 1
       print "    output cluster count: {0:02x}".format(outCount)
       #output cluster list (16 bit words)
       for x in range (0,outCount):
        thisOne, = unpack("<H",r[position : position+2])
        clustersFound.append(r[position+1] + r[position])
        position += 2
        print "        output cluster {0:04x}".format(thisOne)
       clustersFound.append('\x0b\x04')
       print "added special cluster"
       print "Completed Cluster List"
     except:
      print "error parsing Simple Descriptor"
      printData(data)
     print repr(clustersFound)
     for c in clustersFound:
      getAttributes(data, c) # Now, go get the attribute list for the cluster
    elif (clusterId == 0x0006):
     #print "Match Descriptor Request"
     # Match Descriptor Request
     #printData(data)
     pass
    else:
     print ("Unimplemented Cluster ID", hex(clusterId))
     print
   else:
    print ("Unimplemented Profile ID")
  elif(data['id'] == 'route_record_indicator'):
   #print("Route Record Indicator")
   pass
  else:
   print("some other type of packet")
   print(data)
 except:
  print "I didn't expect this error:", sys.exc_info()[0]
  traceback.print_exc()
  
if __name__ == "__main__":
 #------------ XBee Stuff -------------------------
 # this is the /dev/serial/by-id device for the USB card that holds the XBee
 ZIGBEEPORT = "/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_A600eDiR-if00-port0"
 ZIGBEEBAUD_RATE = 9600
 # Open serial port for use by the XBee
 ser = serial.Serial(ZIGBEEPORT, ZIGBEEBAUD_RATE)


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

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

  
 # Create XBee library API object, which spawns a new thread
 zb = ZigBee(ser, callback=messageReceived)
 print "started at ", time.strftime("%A, %B, %d at %H:%M:%S")
 notYet = True;
 firstTime = True;
 while True:
  try:
   if (firstTime):
    print("Wait while I locate the device")
    time.sleep(1)
    # First send a route record request so when the switch responds
    # I can get the addresses out of it
    print "Broadcasting route record request "
    zb.send('tx_explicit',
     dest_addr_long = BROADCAST,
     dest_addr = UNKNOWN,
     src_endpoint = '\x00',
     dest_endpoint = '\x00',
     cluster = '\x00\x32',
     profile = '\x00\x00',
     data = '\x12'+'\x01'
    )
    # if the device is already properly joined, ten seconds should be
    # enough time for it to have responded. So, configure it to
    # report that light has changed state.
    # If it hasn't joined, this will be ignored.
    time.sleep(5)
    print "sending 'configure reporting'"
    zb.send('tx_explicit',
     dest_addr_long = switchLongAddr,
     dest_addr = switchShortAddr,
     src_endpoint = '\x00',
     dest_endpoint = '\x01',
     cluster = '\x00\x06', # cluster I want to deal with
     profile = '\x01\x04', # home automation profile
     data = '\x00' + '\xaa' + '\x06' + '\x00' + '\x00' + '\x00' + '\x10' + '\x00' + '\x00' + '\x00' + '\x40' + '\x00' + '\x00'
    )
    firstTime = False
   print "Enter a number from 0 through 8 to send a command"
   str1 = raw_input("")
   # Turn Switch Off
   if(str1[0] == '0'):
    print 'Turn switch off'
    zb.send('tx_explicit',
     dest_addr_long = switchLongAddr,
     dest_addr = switchShortAddr,
     src_endpoint = '\x00',
     dest_endpoint = '\x01',
     cluster = '\x00\x06', # cluster I want to deal with
     profile = '\x01\x04', # home automation profile
     data = '\x01' + '\x01' + '\x00'
    )
   # Turn Switch On
   if(str1[0] == '1'):
    print 'Turn switch on'
    zb.send('tx_explicit',
     dest_addr_long = switchLongAddr,
     dest_addr = switchShortAddr,
     src_endpoint = '\x00',
     dest_endpoint = '\x01',
     cluster = '\x00\x06', # cluster I want to deal with
     profile = '\x01\x04', # home automation profile
     data = '\x01' + '\x01' + '\x01'
    )
   # Toggle Switch
   elif (str1[0] == '2'):
    zb.send('tx_explicit',
     dest_addr_long = switchLongAddr,
     dest_addr = switchShortAddr,
     src_endpoint = '\x00',
     dest_endpoint = '\x01',
     cluster = '\x00\x06', # cluster I want to deal with
     profile = '\x01\x04', # home automation profile
     data = '\x01' + '\x01' + '\x02'
    )
   # This will dim it to 20/256 over 5 seconds
   elif (str1[0] == '3'):
    print 'Dim it'
    zb.send('tx_explicit',
     dest_addr_long = switchLongAddr,
     dest_addr = switchShortAddr,
     src_endpoint = '\x00',
     dest_endpoint = '\x01',
     cluster = '\x00\x08', # cluster I want to deal with
     profile = '\x01\x04', # home automation profile
     data = '\x01'+'\xaa'+'\x00'+'\x25'+'\x32'+'\x00'
    )
   # This will brighten it up to 100% over 5 seconds
   elif (str1[0] == '4'):
    print 'Bright'
    zb.send('tx_explicit',
     dest_addr_long = switchLongAddr,
     dest_addr = switchShortAddr,
     src_endpoint = '\x00',
     dest_endpoint = '\x01',
     cluster = '\x00\x08', # cluster I want to deal with
     profile = '\x01\x04', # home automation profile
     data = '\x01'+'\xaa'+'\x00'+'\xff'+'\x32'+'\x00'
    )
   elif (str1[0] == '5'):
    print 'Report Switch Status'
    zb.send('tx_explicit',
     dest_addr_long = switchLongAddr,
     dest_addr = switchShortAddr,
     src_endpoint = '\x00',
     dest_endpoint = '\x01',
     cluster = '\x00\x06', # cluster I want to deal with
     profile = '\x01\x04', # home automation profile
     data = '\x00'+'\xaa'+'\x00'+'\x00'+'\x00'
    )
   elif (str1[0] == '6'):
    print 'Get Report from Switch'
    zb.send('tx_explicit',
     dest_addr_long = switchLongAddr,
     dest_addr = switchShortAddr,
     src_endpoint = '\x00',
     dest_endpoint = '\x00',
     cluster = '\x00\x05', # cluster I want to deal with
     profile = '\x00\x00', # home automation profile
     data = switchShortAddr[1]+switchShortAddr[0]
    )
  except IndexError:
   print "empty line, try again"
  except KeyboardInterrupt:
   print "Keyboard interrupt"
   break
  except NameError as e:
   print "NameError:",
   print e.message.split("'")[1]
   traceback.print_exc(file=sys.stdout)
  except:
   print "Unexpected error:", sys.exc_info()[0]
   traceback.print_exc(file=sys.stdout)
   break
   
  sys.stdout.flush() # if you're running non interactive, do this

 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()

No, it isn't pretty, but it has comments.  It should be easy for folk to read and try out, and is the first example of a direct interface to a ZigBee compliant device I've ever seen.  This should take some of the mystery out of the protocol and the controllers that use it.  This code could be expanded to work one of the thermostats, receive from a panic button, or even one of those simple alarm switches.  The XBee does all the really hard stuff and saves us from worrying about it.

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.