diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4555a28 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +*.py[cod] + +# C extensions +*.so + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg +lib +lib64 + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox +nosetests.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Confirm files +cosm.cfg + diff --git a/README.md b/README.md index d2f934f..0609531 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,87 @@ pynest -- a python interface for the Nest Thermostat +================================== by Scott M Baker, smbaker@gmail.com, http://www.smbaker.com/ +API: +---- + +nest.py define Nest class which could be used to communicate with thermostat. + +Comand-line tool: +-------------- Usage: - 'nest.py help' will tell you what to do and how to do it + 'nesttool.py help' will tell you what to do and how to do it Example: - 'nest.py --user joe@user.com --password swordfish temp 73' + 'nesttool.py --user joe@user.com --password swordfish temp 73' set the temperature to 73 degrees - 'nest.py --user joe@user.com --password swordfish fan auto' + 'nesttool.py --user joe@user.com --password swordfish fan auto' set the fan to automatic - Installation: - 'python ./setup.py install' will install nest.py to the right place, + +COSM submission: +--------------- + +'nest_cosm.py' script could be used to submit thermostat data to COSM: http://cosm.com/ + +Usage: + + ./nest_cosm.py [-f ] [-c] [-d]a + + -c -- log to console instead of log file + -d -- dry-run mode. No data submitted. + -f -- config file name. Default is 'cosm.cfg' + -l -- config file name. Default is 'cosm.log' + +Configuration file example: + + { + "key":"your key" + "feed":123, + "nest_user":"user@example.com", + "nest_password":"secret", + "units":"C", + "fields": { + "current_temperature":{"datastream":1}, + "current_humidity":{"datastream":2}, + "fan_mode":{"datastream":3, + "mapping":{ + "off":-1, + "on":1, + "auto":0 + }}, + "hvac_ac_state": {"datastream":4,"mapping":{ + "False":0, + "True":1 + }}, + "hvac_heater_state":{"datastream":5,"mapping":{ + "False":0, + "True":1 + }}, + "battery_level":{"datastream":100} + } + } + +Sample feed: https://cosm.com/feeds/131118 + + + +Installation: +---------- + 'python ./setup.py install' will install nesttool.py and nest_cosm.py to the right place, usually your /usr/bin directory. - Licensing: +Licensing: +--------- This is distributed unider the Creative Commons 3.0 Non-commecrial, Attribution, Share-Alike license. You can use the code for noncommercial purposes. You may NOT sell it. If you do use it, then you must make an attribution to me (i.e. Include my name and thank me for the hours I spent on this) - Acknowledgements: +Acknowledgements: +---------------- Chris Burris's Siri Nest Proxy was very helpful to learn the nest's authentication and some bits of the protocol. diff --git a/cosm.py b/cosm.py new file mode 100644 index 0000000..05c5b63 --- /dev/null +++ b/cosm.py @@ -0,0 +1,33 @@ +""" +Simple API for COSM.com +""" +import urllib2 + +def submit_datapoints(feed,datastream,key,csv): + """ + Submit CSV-formatted list of datapoints to specified datastream + """ + if len(csv)==0: + return + opener = urllib2.build_opener(urllib2.HTTPHandler) + request = urllib2.Request("http://api.cosm.com/v2/feeds/%s/datastreams/%s/datapoints.csv" % (feed,datastream), csv) + request.add_header('Host','api.cosm.com') + request.add_header('Content-type','text/csv') + request.add_header('X-ApiKey', key) + opener.open(request) + + +def update_feed(feed,key,csv): + """ + Submit CSV-formatted data to update the feed. + @see https://cosm.com/docs/v2/feed/update.html + """ + if len(csv)==0: + return + opener = urllib2.build_opener(urllib2.HTTPHandler) + request = urllib2.Request("http://api.cosm.com/v2/feeds/%s?_method=put" % (feed), csv) + request.add_header('Host','api.cosm.com') + request.add_header('Content-type','text/csv') + request.add_header('X-ApiKey', key) + opener.open(request) + diff --git a/nest.py b/nest.py old mode 100755 new mode 100644 index 7fcf863..8379c1b --- a/nest.py +++ b/nest.py @@ -3,9 +3,6 @@ # nest.py -- a python interface to the Nest Thermostat # by Scott M Baker, smbaker@gmail.com, http://www.smbaker.com/ # -# Usage: -# 'nest.py help' will tell you what to do and how to do it -# # Licensing: # This is distributed unider the Creative Commons 3.0 Non-commecrial, # Attribution, Share-Alike license. You can use the code for noncommercial @@ -20,7 +17,6 @@ import urllib import urllib2 import sys -from optparse import OptionParser try: import json @@ -141,97 +137,4 @@ def set_fan(self, state): print res -def create_parser(): - parser = OptionParser(usage="nest [options] command [command_options] [command_args]", - description="Commands: fan temp", - version="unknown") - - parser.add_option("-u", "--user", dest="user", - help="username for nest.com", metavar="USER", default=None) - - parser.add_option("-p", "--password", dest="password", - help="password for nest.com", metavar="PASSWORD", default=None) - - parser.add_option("-c", "--celsius", dest="celsius", action="store_true", default=False, - help="use celsius instead of farenheit") - - parser.add_option("-s", "--serial", dest="serial", default=None, - help="optional, specify serial number of nest thermostat to talk to") - - parser.add_option("-i", "--index", dest="index", default=0, type="int", - help="optional, specify index number of nest to talk to") - - - return parser - -def help(): - print "syntax: nest [options] command [command_args]" - print "options:" - print " --user ... username on nest.com" - print " --password ... password on nest.com" - print " --celsius ... use celsius (the default is farenheit)" - print " --serial ... optional, specify serial number of nest to use" - print " --index ... optional, 0-based index of nest" - print " (use --serial or --index, but not both)" - print - print "commands: temp, fan, show, curtemp, curhumid" - print " temp ... set target temperature" - print " fan [auto|on] ... set fan state" - print " show ... show everything" - print " curtemp ... print current temperature" - print " curhumid ... print current humidity" - print - print "examples:" - print " nest.py --user joe@user.com --password swordfish temp 73" - print " nest.py --user joe@user.com --password swordfish fan auto" - -def main(): - parser = create_parser() - (opts, args) = parser.parse_args() - - if (len(args)==0) or (args[0]=="help"): - help() - sys.exit(-1) - - if (not opts.user) or (not opts.password): - print "how about specifying a --user and --password option next time?" - sys.exit(-1) - - if opts.celsius: - units = "C" - else: - units = "F" - - n = Nest(opts.user, opts.password, opts.serial, opts.index, units=units) - n.login() - n.get_status() - - cmd = args[0] - - if (cmd == "temp"): - if len(args)<2: - print "please specify a temperature" - sys.exit(-1) - n.set_temperature(int(args[1])) - elif (cmd == "fan"): - if len(args)<2: - print "please specify a fan state of 'on' or 'auto'" - sys.exit(-1) - n.set_fan(args[1]) - elif (cmd == "show"): - n.show_status() - elif (cmd == "curtemp"): - n.show_curtemp() - elif (cmd == "curhumid"): - print n.status["device"][n.serial]["current_humidity"] - else: - print "misunderstood command:", cmd - print "do 'nest.py help' for help" - -if __name__=="__main__": - main() - - - - diff --git a/nest_cosm.py b/nest_cosm.py new file mode 100755 index 0000000..ffcacd8 --- /dev/null +++ b/nest_cosm.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python2.7 +""" +This script will read nest thermostat values and will post to COSM.com + +It is using cosm.cfg which is JSON dictionary with following fields: + +{ + "key":"your key" + "feed":123, + "nest_user":"user@example.com", + "nest_password":"secret", + "units":"C", + "fields": { + "current_temperature":{"datastream":1}, + "current_humidity":{"datastream":2}, + "fan_mode":{"datastream":3, + "mapping":{ + "off":-1, + "on":1, + "auto":0 + }}, + "hvac_ac_state": {"datastream":4,"mapping":{ + "False":0, + "True":1 + }}, + "hvac_heater_state":{"datastream":5,"mapping":{ + "False":0, + "True":1 + }}, + "battery_level":{"datastream":100} + } +} +""" + +import json +import sys +import logging +import string +import getopt +import cosm +from nest import Nest + +CFG_FILE="cosm.cfg" +COSM_LOGFILE="cosm.log" + +def usage(): + print """ +%s [-f ] [-c] [-d] + +-c -- log to console instead of log file +-d -- dry-run mode. No data submitted. +-f -- config file name. Default is '%s' +-l -- config file name. Default is '%s' + +""" % (sys.argv[0],CFG_FILE,COSM_LOGFILE) + +def read_config(cfg_fname): + log.info("Reading config file %s" % cfg_fname) + f=open(cfg_fname,"r") + try: + return json.load(f) + finally: + f.close() + +def main(): + global log + global debug_mode + + try: + opts, args = getopt.getopt(sys.argv[1:], 'dcf:l:', []) + except getopt.GetoptError: + usage() + sys.exit(2) + + console = False + debug_mode = False + cfg_fname = CFG_FILE + log_fname = COSM_LOGFILE + + for o, a in opts: + if o in ['-d']: + debug_mode = True + elif o in ['-c']: + console = True + elif o in ['-f']: + cfg_fname = a + elif o in ['-l']: + log_fname = a + else: + usage() + sys.exit(1) + + log_format = '%(asctime)s %(process)d %(filename)s:%(lineno)d %(levelname)s %(message)s' + if debug_mode: + log_level=logging.DEBUG + else: + log_level=logging.INFO + if console: + logging.basicConfig(level=log_level, format=log_format) + else: + logging.basicConfig(level=log_level, format=log_format, + filename=log_fname, filemode='a') + log = logging.getLogger('default') + + try: + cfg = read_config(cfg_fname) + except Exception, ex: + log.error("Error reading config file %s" % ex) + sys.exit(1) + + fields = cfg["fields"] + + try: + n = Nest(cfg["nest_user"],cfg["nest_password"],units=cfg["units"]) + n.login() + n.get_status() + shared = n.status["shared"][n.serial] + device = n.status["device"][n.serial] + allvars = shared + allvars.update(device) + except Exception, ex: + log.error("Error connecting to NEST: %s" % ex ) + sys.exit(100) + + data = "" + for fname,fds in fields.items(): + if allvars.has_key(fname): + ds = str(fds["datastream"]) + if fds.has_key("mapping"): + rv = str(allvars[fname]) + if fds["mapping"].has_key(rv): + v = str(fds["mapping"][rv]) + else: + log.error("Unknown value '%s' for mapped field '%s" % (rv,fname)) + continue + else: + v= str(allvars[fname]) + data = data + string.join([ds,v],",")+"\r\n" + else: + log.warning("Field '%s' not found!", fname) + try: + if not debug_mode: + log.info("Updating feed %s" % cfg["feed"]) + cosm.update_feed(cfg["feed"],cfg["key"],data) + else: + log.debug(data) + except Exception, ex: + log.error("Error sending to COSM: %s" % ex ) + sys.exit(102) + + log.debug("Done") + + +if __name__ == '__main__': + main() diff --git a/nesttool.py b/nesttool.py new file mode 100755 index 0000000..9d8281d --- /dev/null +++ b/nesttool.py @@ -0,0 +1,120 @@ +#! /usr/bin/python + +# nesttool.py -- a python interface to the Nest Thermostat +# by Scott M Baker, smbaker@gmail.com, http://www.smbaker.com/ +# +# Usage: +# 'nest.py help' will tell you what to do and how to do it +# +# Licensing: +# This is distributed unider the Creative Commons 3.0 Non-commecrial, +# Attribution, Share-Alike license. You can use the code for noncommercial +# purposes. You may NOT sell it. If you do use it, then you must make an +# attribution to me (i.e. Include my name and thank me for the hours I spent +# on this) +# +# Acknowledgements: +# Chris Burris's Siri Nest Proxy was very helpful to learn the nest's +# authentication and some bits of the protocol. + +import urllib +import urllib2 +import sys +from optparse import OptionParser + +from nest import Nest + +def create_parser(): + parser = OptionParser(usage="nest [options] command [command_options] [command_args]", + description="Commands: fan temp", + version="unknown") + + parser.add_option("-u", "--user", dest="user", + help="username for nest.com", metavar="USER", default=None) + + parser.add_option("-p", "--password", dest="password", + help="password for nest.com", metavar="PASSWORD", default=None) + + parser.add_option("-c", "--celsius", dest="celsius", action="store_true", default=False, + help="use celsius instead of farenheit") + + parser.add_option("-s", "--serial", dest="serial", default=None, + help="optional, specify serial number of nest thermostat to talk to") + + parser.add_option("-i", "--index", dest="index", default=0, type="int", + help="optional, specify index number of nest to talk to") + + + return parser + +def help(): + print "syntax: nest [options] command [command_args]" + print "options:" + print " --user ... username on nest.com" + print " --password ... password on nest.com" + print " --celsius ... use celsius (the default is farenheit)" + print " --serial ... optional, specify serial number of nest to use" + print " --index ... optional, 0-based index of nest" + print " (use --serial or --index, but not both)" + print + print "commands: temp, fan, show, curtemp, curhumid" + print " temp ... set target temperature" + print " fan [auto|on] ... set fan state" + print " show ... show everything" + print " curtemp ... print current temperature" + print " curhumid ... print current humidity" + print + print "examples:" + print " nest.py --user joe@user.com --password swordfish temp 73" + print " nest.py --user joe@user.com --password swordfish fan auto" + +def main(): + parser = create_parser() + (opts, args) = parser.parse_args() + + if (len(args)==0) or (args[0]=="help"): + help() + sys.exit(-1) + + if (not opts.user) or (not opts.password): + print "how about specifying a --user and --password option next time?" + sys.exit(-1) + + if opts.celsius: + units = "C" + else: + units = "F" + + n = Nest(opts.user, opts.password, opts.serial, opts.index, units=units) + n.login() + n.get_status() + + cmd = args[0] + + if (cmd == "temp"): + if len(args)<2: + print "please specify a temperature" + sys.exit(-1) + n.set_temperature(int(args[1])) + elif (cmd == "fan"): + if len(args)<2: + print "please specify a fan state of 'on' or 'auto'" + sys.exit(-1) + n.set_fan(args[1]) + elif (cmd == "show"): + n.show_status() + elif (cmd == "curtemp"): + n.show_curtemp() + elif (cmd == "curhumid"): + print n.status["device"][n.serial]["current_humidity"] + else: + print "misunderstood command:", cmd + print "do 'nest.py help' for help" + +if __name__=="__main__": + main() + + + + + diff --git a/sample.crontab b/sample.crontab new file mode 100644 index 0000000..753f30b --- /dev/null +++ b/sample.crontab @@ -0,0 +1,2 @@ +# Sample crontab(5). Submit sensor values every 5 minutes +*/5 * * * * nest_cosm.py -f cosm.cfg -l cosm.log diff --git a/setup.py b/setup.py index dbd9861..5352848 100755 --- a/setup.py +++ b/setup.py @@ -8,5 +8,6 @@ author='Scott Baker', author_email='smbaker@gmail.com', url='http://www.smbaker.com/', - scripts=['nest.py'], + py_modules = ['cosm','nest'] + scripts=['nesttool.py','nest_cosm.py'], )