From bfbbb0370efd0270f5cd940a64fec331339e9e5d Mon Sep 17 00:00:00 2001 From: Vadim Zaliva Date: Thu, 9 May 2013 19:34:24 -0700 Subject: [PATCH 1/8] getting vars --- .gitignore | 39 +++++++++++++++ cosm.py | 33 +++++++++++++ nest_cosm.py | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 203 insertions(+) create mode 100644 .gitignore create mode 100644 cosm.py create mode 100755 nest_cosm.py 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/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_cosm.py b/nest_cosm.py new file mode 100755 index 0000000..60db088 --- /dev/null +++ b/nest_cosm.py @@ -0,0 +1,131 @@ +#!/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":1, + "current_humidity":2, + "fan_mode":3, + } +} +""" + +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): + data = data + string.join([str(fds),str(allvars[fname])],",")+"\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() From 03fa8a5923ef18123d54a1183129105d457a1b92 Mon Sep 17 00:00:00 2001 From: Vadim Zaliva Date: Thu, 9 May 2013 19:41:49 -0700 Subject: [PATCH 2/8] experimentally discovered some usefule vars --- nest_cosm.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nest_cosm.py b/nest_cosm.py index 60db088..32caae8 100755 --- a/nest_cosm.py +++ b/nest_cosm.py @@ -14,7 +14,10 @@ "current_temperature":1, "current_humidity":2, "fan_mode":3, - } + "hvac_ac_state": 4, + "hvac_heater_state":5, + "battery_level":100 +} } """ From a28550476b55e4ee5da0f4af68dbaaf06f43346b Mon Sep 17 00:00:00 2001 From: Vadim Zaliva Date: Thu, 9 May 2013 19:48:12 -0700 Subject: [PATCH 3/8] * split nest.py into nest.py (API) and nesttool.py (command line tool). * modified setup.py to install all scripts and modules [not tested yet!] --- nest.py | 97 ------------------------------------------ nesttool.py | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 3 +- 3 files changed, 122 insertions(+), 98 deletions(-) mode change 100755 => 100644 nest.py create mode 100755 nesttool.py 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/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/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'], ) From 39d8e10ff5f00e471a489bd933da24c222a40e17 Mon Sep 17 00:00:00 2001 From: Vadim Zaliva Date: Thu, 9 May 2013 19:58:41 -0700 Subject: [PATCH 4/8] mapped fields --- nest_cosm.py | 49 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/nest_cosm.py b/nest_cosm.py index 32caae8..16e536c 100755 --- a/nest_cosm.py +++ b/nest_cosm.py @@ -5,19 +5,30 @@ 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":1, - "current_humidity":2, - "fan_mode":3, - "hvac_ac_state": 4, - "hvac_heater_state":5, - "battery_level":100 -} + "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":9, + "True":0 + }}, + "hvac_heater_state":{"datastream":5,"mapping":{ + "False":9, + "True":0 + }}, + "battery_level":{"datastream":100} + } } """ @@ -114,7 +125,17 @@ def main(): data = "" for fname,fds in fields.items(): if allvars.has_key(fname): - data = data + string.join([str(fds),str(allvars[fname])],",")+"\r\n" + 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: From 9dd394a31af81296cca427cb97b60245c2de186b Mon Sep 17 00:00:00 2001 From: Vadim Zaliva Date: Thu, 9 May 2013 19:59:32 -0700 Subject: [PATCH 5/8] update readme with new tool name --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d2f934f..d5ab611 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,17 @@ 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 + '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, + 'python ./setup.py install' will install nesttool.py and nest_cosm.py to the right place, usually your /usr/bin directory. Licensing: From d4028b33f7916fc135c00c886f8d38dd04275a1b Mon Sep 17 00:00:00 2001 From: Vadim Zaliva Date: Thu, 9 May 2013 20:05:47 -0700 Subject: [PATCH 6/8] corrected typos in mapping --- nest_cosm.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nest_cosm.py b/nest_cosm.py index 16e536c..ffcacd8 100755 --- a/nest_cosm.py +++ b/nest_cosm.py @@ -20,12 +20,12 @@ "auto":0 }}, "hvac_ac_state": {"datastream":4,"mapping":{ - "False":9, - "True":0 + "False":0, + "True":1 }}, "hvac_heater_state":{"datastream":5,"mapping":{ - "False":9, - "True":0 + "False":0, + "True":1 }}, "battery_level":{"datastream":100} } From 7ba0a4640b6fbe75360c39d3cc422f485a8fa6dc Mon Sep 17 00:00:00 2001 From: Vadim Zaliva Date: Thu, 9 May 2013 20:07:23 -0700 Subject: [PATCH 7/8] sample crontab --- sample.crontab | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 sample.crontab 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 From d21d5316df1c60503b8ec37f8b7ac65acfd26cac Mon Sep 17 00:00:00 2001 From: Vadim Zaliva Date: Thu, 9 May 2013 20:15:58 -0700 Subject: [PATCH 8/8] documentation --- README.md | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d5ab611..0609531 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,14 @@ 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: 'nesttool.py help' will tell you what to do and how to do it @@ -11,18 +19,69 @@ 'nesttool.py --user joe@user.com --password swordfish fan auto' set the fan to automatic - Installation: + +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.