Saturday, June 7, 2014

Taking apart the Iris Smart Switch

I decided to take the plunge and finish off the 120V appliances I want to monitor right now.  There will probably be others in the future, but for now the two freezers and refrigerator will be the devices I keep an eye on.  I also want another Iris Smart Switch to wander around the house with.  It was nice to be able to evaluate the new smoker <link>, and I'm sure I'll want to do that again with some other appliance (TV, wall warts, maybe a Raspberry Pi) from time to time.

This time though, I'll finish the software to monitor the appliances and take the switch apart to modify it to make sure the appliances can't be turned off accidentally.  It's been a concern of mine that the switch can be both locally and remotely controlled and my major appliances are fed through it.  I don't want a power surge or EMF pulse to shut down my freezers.  So, I got out the screwdriver and a knife.  I peeled the label covering one of the screws and took the thing apart.

I was expecting something like the X10 devices I've dismantled over the years, and was I ever surprised, this thing is really well made:


Notice that there's a well laid out circuit board, nice large wires to handle the load, and substantial connectors for the power in and out.   Right off I noticed that they had a resistor in the circuit for power.  Yes, they're using a real shunt resistor to measure power.  Talk about old school, tried and true, easy to work with design.  There's a pretty substantial set of filtering caps to keep noise out of the circuitry and a 16A Omron relay for control.  The point I wanted was where the power is broken by the relay; I just need to short across the relay connections and I'll be done.

For the folk out there that want to see the rest of it, here's the back side of the board:


The relay is a single pole, double throw and they use both sets of contacts.  I was impressed by the construction and may, someday, take the board out and use it for control of something else.  For now, having it at the plug is really great.

After I shorted across the relay pins --- go look up the relay to get the pins <link>, I don't want to be responsible for you messing it up and screwing up the switch --- I put it back together and started working on the code.  The way I did it is, if a new device shows up, I put an entry in my Sqlite3 database holding the fields I'm interested in (all I use) and assigned it the name of 'unknown'.  Then I go ahead and let the switch join.  It will immediately start sending data, and I save it to the new record in the database.  Then using the command line interface for Sqlite3, I just give it a name and it is now part of my network.  If you want to store it in some other fashion, just change the code to accommodate whatever technique you are using.  However, I really like the Sqlite3 database, it serves as storage for all my devices and has worked like a champ for months now.  If you're doing this on an Arduino, you can use the non-volatile storage capability.  I'm not going to port this code to the Arduino though, I have no need for it there.

The Python Script
#! /usr/bin/python
# This is the an implementation of monitoring the Lowe's Iris Smart
# Switch that I use.  It will join with a switch and does NOT allow you 
# to control the switch
#
# This version has been adapted to support more than one switch and will 
# add a new record to my database to hold the data.  Adapt it as you need 
# to.
#
# Have fun

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

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

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

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

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

# this is the only way I could think of to get the address strings to store.
# I take the ord() to get a number, convert to hex, then take the 3 to end
# characters and pad them with zero and finally put the '0x' back on the front
# I put spaces in between each hex character to make it easier to read.  This
# left an extra space at the end, so I slice it off in the return statement.
# I hope this makes it easier to grab it out of the database when needed
def addrToString(funnyAddrString):
 hexified = ''
 for i in funnyAddrString:
  hexified += '0x' + hex(ord(i))[2:].zfill(2) + ' '
 return hexified[:-1]
 

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

# this is a call back function.  When a message
# comes in this function will get the data
def messageReceived(data):
 #print 'gotta packet' 
 #print data
 clusterId = (ord(data['cluster'][0])*256) + ord(data['cluster'][1])
 #print 'Cluster ID:', hex(clusterId),
 if (clusterId == 0x13):
  # This is the device announce message.
  # due to timing problems with the switch itself, I don't 
  # respond to this message, I save the response for later after the
  # Match Descriptor request comes in.  You'll see it down below.
  # if you want to see the data that came in with this message, just
  # uncomment the 'print data' comment up above
  print 'Device Announce Message'
 elif (clusterId == 0x8005):
  # this is the Active Endpoint Response This message tells you
  # what the device can do, but it isn't constructed correctly to match 
  # what the switch can do according to the spec.  This is another 
  # message that gets it's response after I receive the Match Descriptor
  print 'Active Endpoint Response'
 elif (clusterId == 0x0006):
  # Match Descriptor Request; this is the point where I finally
  # respond to the switch.  Several messages are sent to cause the 
  # switch to join with the controller at a network level and to cause
  # it to regard this controller as valid.
  #
  # First the Active Endpoint Request
  payload1 = '\x00\x00'
  zb.send('tx_explicit',
   dest_addr_long = data['source_addr_long'],
   dest_addr = data['source_addr'],
   src_endpoint = '\x00',
   dest_endpoint = '\x00',
   cluster = '\x00\x05',
   profile = '\x00\x00',
   data = payload1
  )
  print 'sent Active Endpoint'
  # Now the Match Descriptor Response
  payload2 = '\x00\x00\x00\x00\x01\x02'
  zb.send('tx_explicit',
   dest_addr_long = data['source_addr_long'],
   dest_addr = data['source_addr'],
   src_endpoint = '\x00',
   dest_endpoint = '\x00',
   cluster = '\x80\x06',
   profile = '\x00\x00',
   data = payload2
  )
  print 'Sent Match Descriptor'
  # Now there are two messages directed at the hardware
  # code (rather than the network code.  The switch has to 
  # receive both of these to stay joined.
  payload3 = '\x11\x01\x01'
  zb.send('tx_explicit',
   dest_addr_long = data['source_addr_long'],
   dest_addr = data['source_addr'],
   src_endpoint = '\x00',
   dest_endpoint = '\x02',
   cluster = '\x00\xf6',
   profile = '\xc2\x16',
   data = payload2
  )
  payload4 = '\x19\x01\xfa\x00\x01'
  zb.send('tx_explicit',
   dest_addr_long = data['source_addr_long'],
   dest_addr = data['source_addr'],
   src_endpoint = '\x00',
   dest_endpoint = '\x02',
   cluster = '\x00\xf0',
   profile = '\xc2\x16',
   data = payload4
  )
  print 'Sent hardware join messages'
  # now that it should have joined, I'll add a record to the database to
  # hold the status.  I'll just name the device 'unknown' so it can 
  # be updated by hand using sqlite3 directly.  If the device already exists,
  # I'll leave the name alone and just use the existing record
  # Yes, this means you'll have to go into the database and assign it a name
  # 
  dbconn = sqlite3.connect(DATABASE)
  c = dbconn.cursor()
  # See if the device is already in the database
  c.execute("select name from smartswitch "
   "where longaddress = ?; ",
   (addrToString(data['source_addr_long']),))
  switchrecord = c.fetchone()
  if switchrecord is not None:
   print "Device %s is rejoining the network" %(switchrecord[0])
  else:
   print "Adding new device"
   c.execute("insert into smartswitch(name,longaddress, shortaddress, status, watts, twatts, utime)"
    "values (?, ?, ?, ?, ?, ?, ?);",
    ('unknown',
    addrToString(data['source_addr_long']),
    addrToString(data['source_addr']),
    'unknown',
    '0',
    '0',
    time.strftime("%A, %B, %d at %H:%M:%S")))
   dbconn.commit()
  dbconn.close()

 elif (clusterId == 0xef):
  clusterCmd = ord(data['rf_data'][2])
  if (clusterCmd == 0x81):
   usage = ord(data['rf_data'][3]) + (ord(data['rf_data'][4]) * 256)
   dbconn = sqlite3.connect(DATABASE)
   c = dbconn.cursor()
   # This is commented out because I don't need the name
   # unless I'm debugging.
   # get device name from database
   #c.execute("select name from smartswitch "
   # "where longaddress = ?; ",
   # (addrToString(data['source_addr_long']),))
   #name = c.fetchone()[0].capitalize()
   #print "%s Instaneous Power, %d Watts" %(name, usage)
   # do database updates
   c.execute("update smartswitch "
    "set watts =  ?, "
    "shortaddress = ?, "
    "utime = ? where longaddress = ?; ",
    (usage, addrToString(data['source_addr']), 
     time.strftime("%A, %B, %d at %H:%M:%S"), addrToString(data['source_addr_long'])))
   dbconn.commit()
   dbconn.close()
  elif (clusterCmd == 0x82):
   usage = (ord(data['rf_data'][3]) +
    (ord(data['rf_data'][4]) * 256) +
    (ord(data['rf_data'][5]) * 256 * 256) +
    (ord(data['rf_data'][6]) * 256 * 256 * 256) )
   upTime = (ord(data['rf_data'][7]) +
    (ord(data['rf_data'][8]) * 256) +
    (ord(data['rf_data'][9]) * 256 * 256) +
    (ord(data['rf_data'][10]) * 256 * 256 * 256) )
   dbconn = sqlite3.connect(DATABASE)
   c = dbconn.cursor()
   c.execute("select name from smartswitch "
    "where longaddress = ?; ",
    (addrToString(data['source_addr_long']),))
   name = c.fetchone()[0].capitalize()
   print "%s Minute Stats: Usage, %d Watt Hours; Uptime, %d Seconds" %(name, usage/3600, upTime)
   # update database stuff
   c.execute("update smartswitch "
    "set twatts =  ?, "
    "shortaddress = ?, "
    "utime = ? where longaddress = ?; ",
    (usage, addrToString(data['source_addr']), 
     time.strftime("%A, %B, %d at %H:%M:%S"), addrToString(data['source_addr_long'])))
   dbconn.commit()
   dbconn.close()
   
 elif (clusterId == 0xf0):
  clusterCmd = ord(data['rf_data'][2])
  # print "Cluster Cmd:", hex(clusterCmd),
  # if (clusterCmd == 0xfb):
   #print "Temperature ??"
  # else:
   #print "Unimplemented"
 elif (clusterId == 0xf6):
  clusterCmd = ord(data['rf_data'][2])
  # if (clusterCmd == 0xfd):
   # pass #print "RSSI value:", ord(data['rf_data'][3])
  # elif (clusterCmd == 0xfe):
   # pass #print "Version Information"
  # else:
   # pass #print data['rf_data']
 elif (clusterId == 0xee):
  clusterCmd = ord(data['rf_data'][2])
  status = ''
  if (clusterCmd == 0x80):
   if (ord(data['rf_data'][3]) & 0x01):
    status = "ON"
   else:
    status = "OFF"
   dbconn = sqlite3.connect(DATABASE)
   c = dbconn.cursor()
   c.execute("select name from smartswitch "
    "where longaddress = ?; ",
    (addrToString(data['source_addr_long']),))
   print c.fetchone()[0].capitalize(),
   print "Switch is", status
   c.execute("update smartswitch "
    "set status =  ?, "
    "shortaddress = ?, "
    "utime = ? where longaddress = ?; ",
    (status, addrToString(data['source_addr']), 
     time.strftime("%A, %B, %d at %H:%M:%S"), addrToString(data['source_addr_long'])))
   dbconn.commit()
   dbconn.close()
 else:
  print "Unimplemented Cluster ID", hex(clusterId)
  print

def sendSwitch(whereLong, whereShort, srcEndpoint, destEndpoint, 
    clusterId, profileId, clusterCmd, databytes):
 
 payload = '\x11\x00' + clusterCmd + databytes
 # print 'payload',
 # for c in payload:
  # print hex(ord(c)),
 # print
 # print 'long address:',
 # for c in whereLong:
  # print hex(ord(c)),
 # print
  
 zb.send('tx_explicit',
  dest_addr_long = whereLong,
  dest_addr = whereShort,
  src_endpoint = srcEndpoint,
  dest_endpoint = destEndpoint,
  cluster = clusterId,
  profile = profileId,
  data = payload
  )
# This just puts a time stamp in the log file for tracking
def timeInLog():
 print time.strftime("%A, %B, %d at %H:%M:%S")
 
#------------------If you want to schedule something to happen -----
scheditem = Scheduler()
scheditem.start()

scheditem.add_interval_job(timeInLog, minutes=15)

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

# 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")
while True:
 try:
  time.sleep(0.1)
  sys.stdout.flush() # if you're running non interactive, do this

 except KeyboardInterrupt:
  print "Keyboard interrupt"
  break
 except:
  print "Unexpected error:", sys.exc_info()[0]
  break

print "After the while loop"
# halt() must be called before closing the serial
# port in order to ensure proper thread shutdown
zb.halt()
ser.close()
Of course, I have to update my storage out there to hold the new appliance and start charting it.  That means, for now, updating my legacy feeds on Xively and then adding the new feed to my appliance chart.  Then when I get (yet) another one of these, I'll have to make similar updates for it.  I know, I could spend a few days coming up with a way to automate this, but why bother?  It only takes an hour or so to add a new monitor device and I don't have any current plans for a new one.  Here's the latest chart, notice I can now monitor the freezer out in the garage.



Sure, the chart looks too busy to understand, but remember you can click on the legend items on the bottom and turn off whatever you don't want to look at right now.  It looks like my garage freezer is roughly equivalent to a 50W light bulb running all the time.  I guess I need to figure out what that costs me, but it takes a spreadsheet to do it.  I have seasonal rates and demand billing, that will take some thought.

So, I'll add one more device to float around the house measuring things that may catch my interest from time to time.  The kill-a-watt is cool, but you don't get a feel for how a particular device will behave over time.  The kill-a-watt didn't tell me that the freezer defrost timer was hitting during the peak period when it could just as well operate during off-peak periods.

Now I have to open the other two devices and install a jumper, have fun.

15 comments:

  1. Does this work with your existing XBee or are you still using two on the RPi?

    ReplyDelete
    Replies
    1. If you mean the code, it's for the Raspberry Pi, but the technique could be adapted really easily to the Arduino. The real change I made was to take the address out of the packet each time instead of storing it. The biggest problem on the Arduino is working up the code to store the switch name and address in the eeprom. You'd actually have to get user input to name it or do some name based on an algorithm.

      Delete
  2. Not what I was asking. I remember when you originally started playing with these, you had to add another XBee just for the Iris devices. I was wondering if you'd found a way to do it all from the same XBee. It's probably not possible but it certainly would be nice.

    ReplyDelete
    Replies
    1. Oh, sorry. I'm still using two XBees, the nature of the XBee setup is such that I can't find any way to do it with one XBee. Glad they're cheap (and I have a few I haven't used yet).

      Delete
  3. Guess I'll just pursue the Z-Wave option then for my electrical outlet control needs. The door locks are working well at the moment and I've got X-Bee traffic going in multiple directions. Need to update my pool controller to take orders from the house controller but that's a project for another day. Getting ready now to replace my sprinkler system timer with one that not only knows that it's raining but will tell me how much rain has fallen and modify the next run time based on that knowledge. The little Arduino's are still getting put to use but they're starting to take orders from the RPi now. Also looking into adding a second RPi to the house with speech synthesis to announce certain events as well as replacing my doorbell chime.....

    ReplyDelete
    Replies
    1. I can't possibly argue with the Z-Wave stuff. It's priced well and seems to work really well for most people. The only objection I have is that they are so proprietary with the stuff, and just don't want to share information. However, my experience with ZigBee has been exactly the same, they hide their stuff under the 'manufacturer specific' clause and don't share either. So, that makes it a wash, and frankly, I'm leaning heavily toward the Z-Wave right now. I'll be looking into that in a while.

      When you look at speech synthesis, let me know if you find anything you can actually understand. My results were really poor. The voice sounded like a bass singer with a very bad cold and I just couldn't understand over half of the words. I'd like to have voice alarms for things around the house, but not with what I've run across recently.

      Delete
    2. And, the combination of RPi and Arduino makes for a killer system. The little arduino can have some autonomy about what it does in default situations, while the RPI is so darn easy to use for web and other interface jobs that it just makes sense to do it that way.

      I'm starting to shy away from ethernet controls in the house. There's too many opportunities for problems using it. My Wemo switches fail to respond some times and it's tough to write code that will notice all the possibilities of failure and allow for it. With my own little network of XBees, things are super easy (after years of learning) to work with.

      Sigh.

      Delete
  4. Hi Dave

    Thanks for this info. I'm currently using the iris system with my vacation home in Barbados. I was wondering if you think this unit could be altered to accept 240v 10amp? I have a ductless split ac which i would like to monitor. Or even better if you might know of the manufacture so i can source a 240v unit?

    Regards,

    Mark

    ReplyDelete
    Replies
    1. I think it probably could be. However, I wouldn't trust it afterwards. A lot of these devices get really hot when they are running at 220, and I just wouldn't want that. A better solution would be to look at the European devices. There's a bunch of them you can get from ebay that are used in the UK. Their not Iris, but have similar capabilities.

      Delete
  5. This works great, I have used these exact same ones in the past and really like how reliable they are. They don't look out of place when installed like others. I like the fact that after a power outage they keep their state, so if it was on before the outage it will be on after the power comes back. That might sound obvious to others but I have other switches that I can't rely on for that reason.

    ReplyDelete
  6. Is it possible to use a iris smart switch as a power usage measuring device, but monitor on an android phone?

    ReplyDelete
    Replies
    1. Not with just the iris switch and a smartphone. You'd need additional hardware and software.

      Delete
  7. The on off button can be disabled by software if placed in lock mode.
    in hubitat
    sendZigbeeCommands(["he raw ${device.deviceNetworkId} 0 ${device.endpointId} 0x00F0 {11 00 FA 02 01} {0xC216}"])

    ReplyDelete
    Replies
    1. That's great to know, thank you. However, I feel much better with a piece of wire in there to be sure. Your suggestion will be great for some other ideas though.

      Delete
  8. You never tried the care switch or the keypad.

    They both use 00C0 and expect a reply command that I can not figure out how to send
    Using groovy on hubitat
    sendZigbeeCommands(["he raw ${device.deviceNetworkId} 0 ${device.endpointId} 0x00C0 {11 00 FA 04 00} {0xC216}"])

    should send my command the iris source code says to send but it doesnt work Any ideals.

    ReplyDelete