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

Sunday, October 12, 2014

Using an 'rc' File in My System

First though, I've been away from my house control system for a while because of other projects, and I decided to do some major updates to portions of it.  Over the months, I've learned a lot and some of that new knowledge needs to be put into controlling the house.  So, yes, I'm posting more often.

Often times, I want to post a new update to one of the processes, but I have keys and other stuff in the modules that have to be removed before I can put it on the web.  I'm also going to move the code into GitHub at some point so folk don't have to copy and paste things for their own use.  They can just grab what they want from there.  That means I have to get the keys out of the code.

Unix has long had a customary way of doing things like this, an rc file.  The 'rc' stands for run command and has a long history, look it up.  There's a lot of files out that go into the user's home directory and have a name like .bashrc, .cshrc, etc.  These are configuration commands and aliases.  This is just the tool I need to use.  I can put an rc file in the home directory and all the keys, database names, etc can go in there to be read at runtime by the applications.  Cool, that way I can make something public without having to worry about somebody updating my data feeds by mistake.

But, I hate parsing data out of a file.  I was looking at parsers and ways of storing data when I got an email about decoding a JSON file.  While I was answering the mail I realized how easy it is in python to parse through JSON, so you guessed it, my rc file is JSON.  That means that it's a tiny bit harder to create and incredibly easy to use in a program.  Here's my .houserc file (with the secrets removed):

{
"database":"/home/pi/database/database",

"xbeeport": "/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_A901QLZ1-if00-port0",
"zigbeeport":"/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_A901QQ2F-if00-port0",

"oldxively":{
"key":"secretkeythingieforlegacyxivelygoeshere",
"feed":"1234"},

"xively":{
"key":"secretkeythingieforthenewinterfacehere",
"feed":"1234567890"},

"emoncms":{
"key":"theyuseashorterkey"},

"grovestreams":{
"org":"grovestream-has-even-stranger-one",
"apiKey":"with-a-strange-key-as-well"},

"thingspeak":{
"key":"ASIMPLEKEYEASYTOUSE"},

"oreo":"double stuff"
}

It's just a simple JSON string that can be easily taken apart.  By using meaningful (to me anyway) names, it's pretty easy to read and edit as well.  To get the data back out in a python program, all you have to do is:

def getHouseValues():
 json_data=open("/home/pi/.houserc").read()
 return json.loads(json_data)
hv = getHouseValues()
FEED_ID = hv["oldxively"]["feed"]
API_KEY = hv["oldxively"]["key"]
# the database where I'm storing stuff
cookie = hv["oreo"]

And, since I don't want to type the directory name over and over again, I put the getHouseValues() in a python file and imported it.

No parsing, no silly obscure things to learn, just things I already had to work with doing this project.  Admittedly, it was a pain going to each module and modifying it to use the rc file, but it was worth it.  Notice I included the weird USB ports I use for the two XBees I have attached?  I can do that with other things that may get changed from time to time.  Just add them to the JSON string and I'll be able to get them back at run-time without changing code in any module.

Heck, it's not even hard to read.

Monday, October 6, 2014

My Tiny Timer Class

Just yesterday I posted about creating a new way of controlling and monitoring my Wemo light switches.  In the post I talked about having to develop a way of timing operations.  It was pretty simple and has worked well overnight, so I decided to put it in a python class and use it in the other parts of my system.

So you understand, I hate classes.  When c++ came around, and object oriented programming was all the rage, I resisted.  No, it wasn't that I couldn't accept change, it was that the crap they produced was incredibly hard to read.  Four pages of class definitions and sixteen lines of code may have been cool, but just try and debug it.  Especially with the tools available back then.  Well, it's been some years and objects have matured as well as the folk that created the mess back then.  Now, they're not as big a mess, maybe I should join the 21st century.  Sure, I use classes all the time; I just avoid writing them if at all possible; this is an exception.

So, here's my basic timer class.  Simple right?  Remember, the reason I did this was to keep control within a single thread thus avoiding deadlocks in code and locked up databases.

import time

class timer:
 _things = []
 
 def __init__(self, callback, seconds=1, minutes=0, hours=0):
  interval = (hours*60*60) + (minutes*60) + seconds
  actionTime = time.time() + interval
  self._things.append({"callback":callback,"interval":interval,"actionTime":actionTime})

 def tick(self):
  now = time.time()
  for i in self._things:
   if i["callback"] == None:
    continue
   if now >= i["actionTime"]:
    i["callback"]()
    i["actionTime"] += i["interval"]

checkTimer = timer(None)

''' This is code to test and illustrate the usage '''
def printSecond():
 print "second"
 
def printTwoSeconds():
 print "twoseconds"

def printMinute():
 print "minute"

if __name__ == "__main__":
 # First create any timers you need
 oneSecond = timer(printSecond, seconds=1)
 twoSeconds = timer(printTwoSeconds, seconds=2)
 minute = timer(printMinute, minutes=1)
 
 # now once in a while call tick to let them happen
 while True:
  checkTimer.tick()
  # a sleep lets the cpu have a rest to do other things.
  time.sleep(0.5)
It just keeps a list of the various timers I create to do things and steps through them to see if something needs to be done.  I included an instance by default so, when I use it, I don't have to create one.  It's easy to use and I may be extending it to handle other things that may need to be timed differently like: Every day at 3:00pm, feed the fish.  That takes more work parsing various parameters and such, but it's easy to extend.

Within the module is an example of how to use it.  This can be directly run under python, to see how it works, and how to use it.

I've already modified my module for the Wemo switches to use it, now I'm looking at the other modules to see if this would be appropriate there as well.  

Sunday, October 5, 2014

Controlling Wemo Light Switches, Another Look

No, I'm not dead, and yes, I still work on electronic projects.  I've been busy with projects that are much more mundane and normal.  Things like trying to build a pad for my motorcycle trailer, fighting with my chlorine generator for the pool, and recovering from my house flooding during the monsoon rains here in AZ.  Yep, I had water come in one side of the house and go out the other.  I'm still working on the damage from that episode.

But people don't read my posts to hear about that kind of thing, they probably have worse problems around the house than I do ... I don't get snow or tornadoes.

For months now I've had Wemo light switches running.  They're a nice little device that Belkin puts out and seem to be gaining in popularity.  Heck, Home Depot sells them in the electrical section now.  However, they rely on an app on the cell phone and a cloud server.  That may not be a concern for most people, but I personally hate having things I depend on subject to the whim of a company that might just decide to start charging me for the service in the future.  That's why I put together some software to run the switches from my Raspberry Pi based house controller <link><link><link>.

However the library and toolset I used, while a great effort and really classy, was too complex and kept failing.  Let me be perfectly clear, the ouimeaux library I used was good, the problem is that the wemo switches don't always work like Universal Plug and Play (upnp) devices are supposed to.  I don't want to get into a lengthy description of upnp here, there are a bunch of those out there, but suffice it to say that the subscription capabilities of the switches fails.

So, you discover the devices, set up a subscription where the devices let you know when the state changes happen, and then sometime later, the switch quits responding.  That means sometime or other, the light doesn't turn on or off and you have to restart the process that controls them to get the switch working again.  Additionally, the ouimeaux library uses a LOT of other tools and provides features that I'm just not interested in.  It uses gevent to handle asynchronous operations, has a little web server, client software, enough stuff for a major project.  I just want to turn them on and off and look to see which way they are now.  Using something large like this to do a simple job leads to huge problems when something fails.  There's just so much there that you can go nuts chasing a problem.

You guessed it, I wrote my own.  Well, that's actually an exaggeration.  I stole some stuff and used it would be closer to the truth.  I started out by writing a handler to discover upnp devices on my network.  If you've never done that, I recommend you look into it just for fun.  I discovered my satellite receiver, my Roku, the DSL modem, all the Wemo switches, and several other devices that I didn't know had plug and play interfaces.  That was a royal pain.  I had to learn about SSDP, SOAP, multicast, XML, and the list was growing.  Shoot, all I want to do was control the darn switches, not create a dissertation on half the jargon on the web.  I did learn a lot about discovering the devices on my network, but that wasn't my goal.

That led me to a few days of searching on the web for upnp libraries.  They're out there, and some of them are really good, but they're written in something besides python.  Sure a couple  have interfaces, but the documentation went totally over my head.  There were some in python, but a couple had disappeared off the web, and one just didn't work (the samples even had syntax errors).  What should I do?  I remembered reading somewhere that python can import modules, and that the python tool Miranda worked reasonably well to experiment with upnp devices ... hmmm.

I got the latest Miranda and combined it with my own code and came up with a way to read and control the switches that uses only the basic python libraries.  It was a challenge hooking into someone else's code that was designed to run as a standalone tool, but it worked.  That way I leveraged the huge amount of work the author of Miranda did to make my job easier.  The big bonus was that Miranda is also really good as a upnp learning tool.  Ha, I have a debugger and a controller all in one.

Since I had spent a lot of time debugging the switch operation to determine why they would quit, I didn't want to use the subscription service.  The subscription facility is asynchronous, so the switch will send an XML message over UDP to a registered URL any time the state changes (off to on); too bad it doesn't work all the time.  That means that I would have to poll the switches to see if something or someone other than my code on the controller had changed their state.  Let me elaborate on that a bit.  These are wall switches; I can walk over and push the button and turn off the lights.  Since my software can't see that, it has to ask the switch what state it is in right now.

That's fine, I can poll it every couple of seconds and see what is happening, BUT there's a problem with that.  I love APScheduler a great timer tool in python; I have it in almost everything I do, but it uses threads to control when things happen.  Using threads and database updates is a path to disaster.  When I tried my new code out, about half the time it would leave the database locked because something jerked execution away at an inopportune time.  I even updated my code and libraries to the latest version of APScheduler which allows jobs to be paused, and still had the problem.  That meant that I had to figure out how to schedule polling of the switches within a single thread.

After a few magnificent failures I came up with a pretty simple way to do it; my method is in the code below.  I may have to use this in some of the previous work I've done, it's really small and simple, so it could decrease the load on the poor little Pi.

Even though this took an inordinate amount of time and study, it's still pretty darn nice.  I haven't ran it for months, but it's held up for a short time pretty well.  I could well develop problems with sockets hanging or something, but the code is simple and straight forward, so I should be able to isolate whatever problems turn up and fix them.  I don't have eight libraries of very complex code written by someone else to step through trying to find out how something works.  It's me and Miranda here, ... well there are the system libraries, but nothing terribly exotic.

Here's the code.  It's written in my usual style: variable names that are too darn long, comments for practically every darn line, over indentation, etc.  Basically everything I need to pick it up a year from now to fix a bug or add a feature.

#! /usr/bin/python
from miranda import upnp
from miranda import msearch
from miranda import set
import sys
#from apscheduler.schedulers.blocking import BlockingScheduler
import datetime
from datetime import timedelta
from datetime import datetime
import time
import sysv_ipc
import logging
import sqlite3
import pdb #yes, I had trouble and had to use this !!
#-------------------------------------------------
# the database where I'm storing stuff
DATABASE='/home/pi/database/desert-home'

def lprint(text):
 print time.strftime("%A, %B, %d at %H:%M:%S"),text
 sys.stdout.flush()
 
def _send(action, whichone, args):
 if not args:
  args = {}
 entry = (item for item in lightSwitches if item["name"] == whichone).next()
 index =entry['index']
 host_info = conn.ENUM_HOSTS[index]
 device_name = 'lightswitch'
 service_name = 'basicevent'
 controlURL = host_info['proto'] + host_info['name']
 controlURL2 = host_info['deviceList'][device_name]['services'][service_name]['controlURL']
 if not controlURL.endswith('/') and not controlURL2.startswith('/'):
  controlURL += '/'
 controlURL += controlURL2

 resp = conn.sendSOAP(
  host_info['name'],
  'urn:Belkin:service:basicevent:1',
  controlURL,
  action,
  args
 )
 return resp
 
def get(whichone):
 ''' 
 Returns True if the light is on and False if not
 '''
 resp = _send('GetBinaryState', whichone, {})
 tagValue = conn.extractSingleTag(resp, 'BinaryState')
 return 'On' if tagValue == '1' else 'Off'

def handleUpdate(whichone, status):
 for i in lightSwitches:
  if i['name'] == whichone:
   i['status'] = status
 updateDatabase(whichone, status)

def on(whichone):
 """
 BinaryState is set to 'Error' in the case that it was already on.
 """
 resp = _send('SetBinaryState', whichone, {'BinaryState': (1, 'Boolean')})
 tagValue = conn.extractSingleTag(resp, 'BinaryState')
 status = 'On' if tagValue in ['1', 'Error'] else 'Off'
 handleUpdate(whichone, status)
 lprint("turned %s on"%(whichone))
 return status

def off(whichone):
 """
 BinaryState is set to 'Error' in the case that it was already off.
 """
 resp = _send('SetBinaryState', whichone, {'BinaryState': (0, 'Boolean')})
 tagValue = conn.extractSingleTag(resp, 'BinaryState')
 status = 'Off' if tagValue in ['0', 'Error'] else 'On'
 handleUpdate(whichone, status)
 lprint("turned %s off"%(whichone))
 return status

def doLights():
 for switch in lightSwitches:
  thisOne = switch['name']
  updateDatabase(thisOne,get(thisOne))

def doComm():
 global firstTime
 #global scheditem
 
 try:
  if (firstTime):
   while(True):
    try:
     # commands could have piled up while this was 
     # not running. Clear them out.
     junk = Cqueue.receive(block=False, type=0)
     print "purging leftover commands", str(junk)
    except sysv_ipc.BusyError:
     break
   firstTime=False
  while(True):
   newCommand = Cqueue.receive(block=False, type=0)
   # type=0 above means suck every message off the
   # queue.  If I used a number above that, I'd
   # have to worry about the type in other ways.
   # note, I'm reserving type 1 messages for 
   # test messages I may send from time to 
   # time.  Type 2 are messages that are
   # sent by the php code in the web interface.
   # Type 3 are from the event handler. This is just like
   # the house monitor code in that respect.
   # I haven't decided on any others yet.
   handleCommand(newCommand)
 except sysv_ipc.BusyError:
  pass # Only means there wasn't anything there 


def handleCommand(command):
 #lprint(" " + str(command))
 # the command comes in from php as something like
 # ('s:17:"AcidPump, pumpOff";', 2)
 # so command[0] is 's:17:"AcidPump, pumpOff'
 # then split it at the "  and take the second item
 try:
  c = str(command[0].split('\"')[1]).split(',')
 except IndexError:
  c = str(command[0]).split(' ')   #this is for something I sent from another process
 #lprint(c)
 if (c[0] == 'OutsideLightsOn'):
  outsideLightsOn()
 elif (c[0] == 'OutsideLightsOff'):
  outsideLightsOff()
 elif (c[0] == 'fPorchToggle'):
  toggle("frontporch")
 elif(c[0] == 'garageToggle'):
  toggle("outsidegarage")
 elif (c[0] == 'cactusToggle'):
  toggle("cactusspot")
 elif (c[0] == 'patioToggle'):
  toggle("patio")
 else:
  lprint(" Weird command = " + str(c))

def outsideLightsOn():
 lprint (" Outside lights on")
 on("outsidegarage")
 on("frontporch")
 on("cactusspot")
 
def outsideLightsOff():
 lprint (" Outside lights off")
 off("outsidegarage")
 off("frontporch")
 off("cactusspot")

def toggle(whichOne):
 if (get(whichOne) == 'On'):
  off(whichOne)
 else:
  on(whichOne)
  
def keepAlive():
 '''
 For my own purposes, I update the database periodically with the time
 so I can check to see if things are holding together.  I currently use the
 time in the light switch records for this.
 '''
 lprint(" keep alive")
 for switch in lightSwitches:
  thisOne = switch['name']
  updateDatabase(thisOne, get(thisOne), force=True)

def updateDatabase(whichone, status, force=False):
 ''' 
 This is running on a Pi and is not event driven, so polling like
 this will result in considerable wear to the SD card.  So, I'm going to 
 read the database to see if it needs to be changed before I change it.  
 According to everything I've read, reads are free, it's the writes that
 eat up the card.
 '''
 dbconn = sqlite3.connect(DATABASE)
 c = dbconn.cursor()
 c.execute("select status from lights where name = ?;",
  (whichone,))
 oldstatus = c.fetchone()
 if oldstatus[0] != status or force == True:
  lprint ("Had to update database %s, %s"%(whichone, status))
  c.execute("update lights " 
   "set status = ?, utime = ? where name = ?;",
   (status, time.strftime("%A, %B, %d at %H:%M:%S"), whichone))
  dbconn.commit()
 dbconn.close()
  
if __name__ == "__main__":
 #When looking at a log, this will tell me when it is restarted
 lprint (" started")
 firstTime = True
 debug = False
 if not debug:
  conn = upnp(False,False,None,0)
  ''' 
  I don't want the search for devices to run forever 
  So, I set the timeout for miranda to some number of seconds
  to limit it.
  '''
  set(3, ["set","timeout", "10"], conn)
  ''' 
  This looks at the devices that responded and gathers more data about
  them by sending them a request to itemize their capabilities.

  Sometimes a upnp device goes nuts and responds way out of
  proportion.  You can get the same device in the tables
  many times, so set the uniq to True
  
  Also, the Wemo switches don't always respond to a discover specific to 
  them.  That means I have to do a general discover and get all the devices
  on the network.  This sucks because it slows things down, so if anyone
  overcomes this problem, let me know how.
  '''
  set(3, ["set","uniq", True], conn)
  ''' This is the actual search '''
  msearch(1,[msearch],conn)
  ''' and now do the interaction '''
  for index, hostInfo in conn.ENUM_HOSTS.iteritems():
   #print "************** ", index, " of ", len(conn.ENUM_HOSTS) - 1
   ''' on my network, I have a rogue device that reports badly '''
   if hostInfo['name'].find('192.168.16.254') == 0:
    print "Odd device, ignoring"
    continue
   ''' if you want to see them as they come in, uncomment this '''
   #print hostInfo
   if hostInfo['dataComplete'] == False:
    xmlHeaders, xmlData = conn.getXML(hostInfo['xmlFile'])
    conn.getHostInfo(xmlData,xmlHeaders,index)
    
  ''' 
  now to select only the light switches from the various devices 
  that responded 
  '''
  lightSwitches=[]
  for index, host_info in conn.ENUM_HOSTS.iteritems():
   if "deviceList" in host_info:
    if "lightswitch" in host_info["deviceList"]:
     name = host_info["deviceList"]["lightswitch"]["friendlyName"]
     lightSwitches.append({"name": name, "index": index, "status" : 'unknown'})
  ''' 
  OK, now I have the list of Wemo light switches that are around the 
  house, so print it and show the state of each one 
  '''
  print "this is the list of the", len(lightSwitches), "Wemo switches found."
  for switch in lightSwitches:
   switch['status'] = get(switch['name'])
   print switch
   
 # Create the message queue where commands can be read
 # I just chose an identifier of 13 because the house monitor
 # already took the number 12.
 Cqueue = sysv_ipc.MessageQueue(13, sysv_ipc.IPC_CREAT,mode=0666)
 '''
 This is a poor man's timer for task control.  I may put this in a class
 after I've run it for a while.  The reason I did it this way is that 
 APSscheduler creates a separate thread to handle timers and I don't 
 want the contention to the database of separate threads doing things
 that way.
 
 To use it, just put another entry into the table.
 '''
 lprint (" Setting up timed items")
 startTime = int(time.time())
 tasks = [{'whichOne': doLights,'interval' : 2, 'next': startTime+10},
  {'whichOne': keepAlive, 'interval' : (4*60), 'next' : startTime+15}]
 lprint ("going into the processing loop")
 print startTime
 while True:
  
  #pdb.set_trace()
  now = int(time.time())
  for task in tasks:
   if task['next'] <= now:
    task['next'] = now + task['interval']
    #print task['whichOne']
    task['whichOne']()
  doComm()
  ''' 
  doing a sleep here releases the cpu for longer than the program runs
  That way I reduce the load on the machine so it can do more stuff
  '''
  time.sleep(0.25) 
  pass 
 
 sys.exit("done");

It's only fair to thank Ian McCracken who wrote the Ouimeaux library, Isaac Kelly who did much of the initial discovery work, and Craig Heffner the author of Miranda.  My work is built on theirs.

No, I don't have support in here for the Wemo motion sensor, crock pot, or other items they may come up with.  If you want to grab it and extend it to support those devices, feel free; just let me know so I can get the improvements as well.

There's also support in here for the sysv messages I use to communicate between processes on the machine.  Feel free to rip that out and put in anything you want.  I wanted to give you an example that I'm actually running right now, not something I cut up to look pretty.

Just for fun, here's the output of the code that is captured to my log:

Sunday, October, 05 at 16:22:06  started
action timeout
action uniq
Show unique hosts set to: True
argc 1
argv [<function msearch at 0xb69d94f0>]
Entering discovery mode for 'upnp:rootdevice', Ctl+C to stop...

Error updating command completer structure; some command completion features mig
ht not work...
****************************************************************
SSDP reply message from 192.168.0.34:49312
XML file is located at http://192.168.0.34:49312/device.xml
Device is running Linux/2.6.37.6-4.0, UPnP/1.0, Portable SDK for UPnP devices/1.
6.17
****************************************************************

Error updating command completer structure; some command completion features mig
ht not work...
****************************************************************
SSDP reply message from 192.168.0.34:49200
XML file is located at http://192.168.0.34:49200/device.xml
Device is running Linux/2.6.37.6-4.0, UPnP/1.0, Portable SDK for UPnP devices/1.
6.17
****************************************************************

Error updating command completer structure; some command completion features mig
ht not work...
****************************************************************
SSDP reply message from 192.168.0.28:49154
XML file is located at http://192.168.0.28:49154/setup.xml
Device is running Unspecified, UPnP/1.0, Unspecified
****************************************************************

Error updating command completer structure; some command completion features mig
ht not work...
****************************************************************
SSDP reply message from 192.168.0.29:49153
XML file is located at http://192.168.0.29:49153/setup.xml
Device is running Unspecified, UPnP/1.0, Unspecified
****************************************************************

Error updating command completer structure; some command completion features mig
ht not work...
****************************************************************
SSDP reply message from 192.168.0.43:49154
XML file is located at http://192.168.0.43:49154/setup.xml
Device is running Unspecified, UPnP/1.0, Unspecified
****************************************************************

Error updating command completer structure; some command completion features mig
ht not work...
****************************************************************
SSDP reply message from 192.168.0.26:49153
XML file is located at http://192.168.0.26:49153/setup.xml
Device is running Unspecified, UPnP/1.0, Unspecified
****************************************************************

Error updating command completer structure; some command completion features mig
ht not work...
****************************************************************
SSDP reply message from 192.168.0.22:2869
XML file is located at http://192.168.0.22:2869/upnphost/udhisapi.dll?content=uu
id:3a682b82-e803-4105-bfeb-114ed775cab1
Device is running Microsoft-Windows/6.3 UPnP/1.0 UPnP-Device-Host/1.0
****************************************************************

Error updating command completer structure; some command completion features mig
ht not work...
****************************************************************
SSDP reply message from 192.168.0.14:8060
XML file is located at http://192.168.0.14:8060/
Device is running Roku UPnP/1.0 MiniUPnPd/1.4
****************************************************************

Error updating command completer structure; some command completion features mig
ht not work...
****************************************************************
SSDP reply message from 192.168.0.1:80
XML file is located at http://192.168.0.1:80/DeviceDescription.xml
Device is running ZyXEL-RomPlug/4.51 UPnP/1.0 IGD/1.00
****************************************************************


Discover mode halted...
Failed to get argument name for OpenInstaAP
Failed to get argument name for CloseInstaAP
Failed to get argument name for OpenInstaAP
Failed to get argument name for CloseInstaAP
Failed to get argument name for OpenInstaAP
Failed to get argument name for CloseInstaAP
Failed to get argument name for OpenInstaAP
Failed to get argument name for CloseInstaAP
Caught exception while parsing device service list: list index out of range
At index  7
Caught exception while parsing device service list: list index out of range
At index  8
this is the list of the 4 Wemo switches found.
{'status': 'Off', 'index': 2, 'name': 'cactusspot'}
{'status': 'Off', 'index': 3, 'name': 'frontporch'}
{'status': 'Off', 'index': 4, 'name': 'patio'}
{'status': 'Off', 'index': 5, 'name': 'outsidegarage'}
Sunday, October, 05 at 16:22:42  Setting up timed items
Sunday, October, 05 at 16:22:42 going into the processing loop
1412551362
Sunday, October, 05 at 16:22:56 Had to update database patio, On
Sunday, October, 05 at 16:22:56 turned patio on
Sunday, October, 05 at 16:22:57  keep alive
Sunday, October, 05 at 16:22:57 Had to update database cactusspot, Off
Sunday, October, 05 at 16:22:57 Had to update database frontporch, Off
Sunday, October, 05 at 16:22:57 Had to update database patio, On
Sunday, October, 05 at 16:22:58 Had to update database outsidegarage, Off
Sunday, October, 05 at 16:23:04 Had to update database patio, Off
Sunday, October, 05 at 16:23:04 turned patio off
Sunday, October, 05 at 16:23:12  Outside lights on
Sunday, October, 05 at 16:23:12 Had to update database outsidegarage, On
Sunday, October, 05 at 16:23:12 turned outsidegarage on
Sunday, October, 05 at 16:23:12 Had to update database frontporch, On
Sunday, October, 05 at 16:23:12 turned frontporch on
Sunday, October, 05 at 16:23:12 Had to update database cactusspot, On
Sunday, October, 05 at 16:23:12 turned cactusspot on
Sunday, October, 05 at 16:23:17  Outside lights off
Sunday, October, 05 at 16:23:17 Had to update database outsidegarage, Off
Sunday, October, 05 at 16:23:17 turned outsidegarage off
Sunday, October, 05 at 16:23:17 Had to update database frontporch, Off
Sunday, October, 05 at 16:23:17 turned frontporch off
Sunday, October, 05 at 16:23:17 Had to update database cactusspot, Off
Sunday, October, 05 at 16:23:17 turned cactusspot off
At the top is where I set some parameters in Miranda to control what I discover.  Then the discovery process prints out a summary of the devices it finds. The errors there are caused by the Wemo switches not responding properly.  Next are messages from the conversation with the switches.  Once again, the switches don't behave.  Finally the last step of initialization where the status and names of the switches show up.  The rest of the log is normal operation.  I poll the switches every two seconds to see if they're on or off. and every four minutes to make sure the database is kept current.  I use database currency to monitor the health of the controller.

If you have these switches, or are thinking about using them, grab the code and start playing.