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:
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.
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.
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:
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)
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 'long address:',
# for c in whereLong:
# print hex(ord(c)),
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.