diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad2ae15 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +*.py[cod] + +# C extensions +*.so + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg +lib +lib64 +__pycache__ + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox +nosetests.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +MANIFEST \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..7c178cb --- /dev/null +++ b/LICENSE.md @@ -0,0 +1 @@ +Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..c28ab72 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include README.md +include LICENSE.md diff --git a/README.md b/README.md index d2f934f..ce56564 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,47 @@ - pynest -- a python interface for the Nest Thermostat - by Scott M Baker, smbaker@gmail.com, http://www.smbaker.com/ +#nest_thermostat - Usage: - 'nest.py help' will tell you what to do and how to do it +**a Python interface for the Nest Thermostat** + +*fork of pynest by Scott M Baker, smbaker@gmail.com, http://www.smbaker.com/* - Example: - 'nest.py --user joe@user.com --password swordfish temp 73' - set the temperature to 73 degrees +##Installation +`[sudo] pip install nest_thermostat` - 'nest.py --user joe@user.com --password swordfish fan auto' - set the fan to automatic +##Usage - Installation: - 'python ./setup.py install' will install nest.py to the right place, - usually your /usr/bin directory. +### Module - 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) +You can import the module as `nest_thermostat`. Use the source, luke! - Acknowledgements: - Chris Burris's Siri Nest Proxy was very helpful to learn the nest's - authentication and some bits of the protocol. +Tips: you need to manually call `.login()` first, and `.get_status()` before `.show_*()` +### Command line +``` +syntax: nest.py [options] command [command_args] +options: + --user ... username on nest.com + --password ... password on nest.com + --celsius ... use celsius (the default is farenheit) + --serial ... optional, specify serial number of nest to use + --index ... optional, 0-based index of nest + (use --serial or --index, but not both) + +commands: + temp ... set target temperature + fan [auto|on] ... set fan state + mode [cool|heat|range|off] ... set fan state + away ... toggle away + show ... show everything + curtemp ... print current temperature + curhumid ... print current humidity + curmode ... print current mode + +examples: + nest.py --user joe@user.com --password swordfish temp 73 + nest.py --user joe@user.com --password swordfish fan auto +``` + + +--- + +*Chris Burris's Siri Nest Proxy was very helpful to learn the Nest's authentication and some bits of the protocol.* diff --git a/nest.py b/nest.py index 7fcf863..5e523f6 100755 --- a/nest.py +++ b/nest.py @@ -1,145 +1,12 @@ #! /usr/bin/python -# 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 -# 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 +""" +nest.py -- a python interface to the Nest Thermostat +""" + import sys from optparse import OptionParser - -try: - import json -except ImportError: - try: - import simplejson as json - except ImportError: - print "No json library available. I recommend installing either python-json" - print "or simpejson." - sys.exit(-1) - -class Nest: - def __init__(self, username, password, serial=None, index=0, units="F"): - self.username = username - self.password = password - self.serial = serial - self.units = units - self.index = index - - def loads(self, res): - if hasattr(json, "loads"): - res = json.loads(res) - else: - res = json.read(res) - return res - - def login(self): - data = urllib.urlencode({"username": self.username, "password": self.password}) - - req = urllib2.Request("https://home.nest.com/user/login", - data, - {"user-agent":"Nest/1.1.0.10 CFNetwork/548.0.4"}) - - res = urllib2.urlopen(req).read() - - res = self.loads(res) - - self.transport_url = res["urls"]["transport_url"] - self.access_token = res["access_token"] - self.userid = res["userid"] - - def get_status(self): - req = urllib2.Request(self.transport_url + "/v2/mobile/user." + self.userid, - headers={"user-agent":"Nest/1.1.0.10 CFNetwork/548.0.4", - "Authorization":"Basic " + self.access_token, - "X-nl-user-id": self.userid, - "X-nl-protocol-version": "1"}) - - res = urllib2.urlopen(req).read() - - res = self.loads(res) - - self.structure_id = res["structure"].keys()[0] - - if (self.serial is None): - self.device_id = res["structure"][self.structure_id]["devices"][self.index] - self.serial = self.device_id.split(".")[1] - - self.status = res - - #print "res.keys", res.keys() - #print "res[structure][structure_id].keys", res["structure"][self.structure_id].keys() - #print "res[device].keys", res["device"].keys() - #print "res[device][serial].keys", res["device"][self.serial].keys() - #print "res[shared][serial].keys", res["shared"][self.serial].keys() - - def temp_in(self, temp): - if (self.units == "F"): - return (temp - 32.0) / 1.8 - else: - return temp - - def temp_out(self, temp): - if (self.units == "F"): - return temp*1.8 + 32.0 - else: - return temp - - def show_status(self): - shared = self.status["shared"][self.serial] - device = self.status["device"][self.serial] - - allvars = shared - allvars.update(device) - - for k in sorted(allvars.keys()): - print k + "."*(32-len(k)) + ":", allvars[k] - - def show_curtemp(self): - temp = self.status["shared"][self.serial]["current_temperature"] - temp = self.temp_out(temp) - - print "%0.1f" % temp - - def set_temperature(self, temp): - temp = self.temp_in(temp) - - data = '{"target_change_pending":true,"target_temperature":' + '%0.1f' % temp + '}' - req = urllib2.Request(self.transport_url + "/v2/put/shared." + self.serial, - data, - {"user-agent":"Nest/1.1.0.10 CFNetwork/548.0.4", - "Authorization":"Basic " + self.access_token, - "X-nl-protocol-version": "1"}) - - res = urllib2.urlopen(req).read() - - print res - - def set_fan(self, state): - data = '{"fan_mode":"' + str(state) + '"}' - req = urllib2.Request(self.transport_url + "/v2/put/device." + self.serial, - data, - {"user-agent":"Nest/1.1.0.10 CFNetwork/548.0.4", - "Authorization":"Basic " + self.access_token, - "X-nl-protocol-version": "1"}) - - res = urllib2.urlopen(req).read() - - print res +from nest_thermostat import Nest def create_parser(): parser = OptionParser(usage="nest [options] command [command_options] [command_args]", @@ -165,25 +32,29 @@ def create_parser(): 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" + print( "syntax: nest.py [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:") + print( " temp ... set target temperature") + print( " fan [auto|on] ... set fan state") + print( " mode [cool|heat|range|off] ... set mode state") + print( " away ... toggle away") + print( " show ... show everything") + print( " curtemp ... print current temperature") + print( " curhumid ... print current humidity") + print( " curmode ... print current mode") + print( " curtarget ... print current target temp") + 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() @@ -194,7 +65,7 @@ def main(): sys.exit(-1) if (not opts.user) or (not opts.password): - print "how about specifying a --user and --password option next time?" + print( "how about specifying a --user and --password option next time?") sys.exit(-1) if opts.celsius: @@ -210,28 +81,34 @@ def main(): if (cmd == "temp"): if len(args)<2: - print "please specify a temperature" + 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'" + print( "please specify a fan state of 'on' or 'auto'") sys.exit(-1) n.set_fan(args[1]) + elif (cmd == "mode"): + if len(args)<2: + print( "valid modes are cool, heat, range, and off") + sys.exit(-1) + n.set_mode(args[1]) + elif (cmd == "away"): + n.toggle_away() elif (cmd == "show"): n.show_status() elif (cmd == "curtemp"): n.show_curtemp() + elif (cmd == "curmode"): + n.show_curmode() + elif (cmd == "curtarget"): + n.show_target() elif (cmd == "curhumid"): - print n.status["device"][n.serial]["current_humidity"] + print( n.status["device"][n.serial]["current_humidity"]) else: - print "misunderstood command:", cmd - print "do 'nest.py help' for help" + print( "misunderstood command: %s" % cmd) + print( "do 'nest.py help' for help") if __name__=="__main__": main() - - - - - diff --git a/nest_thermostat/__init__.py b/nest_thermostat/__init__.py new file mode 100644 index 0000000..9742c1c --- /dev/null +++ b/nest_thermostat/__init__.py @@ -0,0 +1,149 @@ +#! /usr/bin/python + +""" +nest_thermostat -- a python interface to the Nest Thermostat +by Scott M Baker, smbaker@gmail.com, http://www.smbaker.com/ +updated by Bob Pasker bob@pasker.net http://pasker.net +""" + +import requests + +try: + import json +except ImportError: + import simplejson as json + +class Nest: + def __init__(self, username, password, serial=None, index=0, units="F", debug=False): + self.username = username + self.password = password + self.serial = serial + self.units = units + self.index = index + self.debug = debug + + def login(self): + + response = requests.post("https://home.nest.com/user/login", + data = {"username":self.username, "password" : self.password}, + headers = {"user-agent":"Nest/1.1.0.10 CFNetwork/548.0.4"}) + + response.raise_for_status() + + res = response.json() + self.transport_url = res["urls"]["transport_url"] + self.access_token = res["access_token"] + self.userid = res["userid"] + # print self.transport_url, self.access_token, self.userid + + def get_status(self): + response = requests.get(self.transport_url + "/v2/mobile/user." + self.userid, + headers={"user-agent":"Nest/1.1.0.10 CFNetwork/548.0.4", + "Authorization":"Basic " + self.access_token, + "X-nl-user-id": self.userid, + "X-nl-protocol-version": "1"}) + + response.raise_for_status() + res = response.json() + + self.structure_id = list(res["structure"].keys())[0] + + if (self.serial is None): + self.device_id = res["structure"][self.structure_id]["devices"][self.index] + self.serial = self.device_id.split(".")[1] + + self.status = res + + #print "res.keys", res.keys() + #print "res[structure][structure_id].keys", res["structure"][self.structure_id].keys() + #print "res[device].keys", res["device"].keys() + #print "res[device][serial].keys", res["device"][self.serial].keys() + #print "res[shared][serial].keys", res["shared"][self.serial].keys() + + def temp_in(self, temp): + if (self.units == "F"): + return (temp - 32.0) / 1.8 + else: + return temp + + def temp_out(self, temp): + if (self.units == "F"): + return temp*1.8 + 32.0 + else: + return temp + + def show_status(self): + shared = self.status["shared"][self.serial] + device = self.status["device"][self.serial] + + allvars = shared + allvars.update(device) + + for k in sorted(allvars): + v = allvars[k] or '' + print("%s: %s" % (k, v)) + + def show_curtemp(self): + temp = self.status["shared"][self.serial]["current_temperature"] + temp = self.temp_out(temp) + + print("%0.1f" % temp) + + def show_target(self): + temp = self.status["shared"][self.serial]["target_temperature"] + temp = self.temp_out(temp) + + print( temp) + + def show_curmode(self): + mode = self.status["shared"][self.serial]["target_temperature_type"] + + print( mode) + + def _set(self, data, which): + if (self.debug): print( json.dumps(data)) + url = "%s/v2/put/%s.%s" % (self.transport_url, which, self.serial) + if (self.debug): print( url) + response = requests.post(url, + data = json.dumps(data), + headers = {"user-agent":"Nest/1.1.0.10 CFNetwork/548.0.4", + "Authorization":"Basic " + self.access_token, + "X-nl-protocol-version": "1"}) + + if response.status_code > 200: + if (self.debug): print( response.content) + response.raise_for_status() + return response + + def _set_shared(self, data): + self._set(data, "shared") + + def _set_device(self, data): + self._set(data, "device") + + def set_temperature(self, temp): + return self._set_shared({ + "target_change_pending": True, + "target_temperature" : self.temp_in(temp) + }) + + def set_fan(self, state): + return self._set_device({ + "fan_mode": str(state) + }) + + def set_mode(self, state): + return self._set_shared({ + "target_temperature_type": str(state) + }) + + def toggle_away(self): + was_away = self.status['structure'][self.structure_id]['away'] + data = '{"away":%s}' % ('false' if was_away else 'true') + response = requests.post(self.transport_url + "/v2/put/structure." + self.structure_id, + data = data, + headers = {"user-agent":"Nest/1.1.0.10 CFNetwork/548.0.4", + "Authorization":"Basic " + self.access_token, + "X-nl-protocol-version": "1"}) + response.raise_for_status() + return response diff --git a/setup.py b/setup.py index dbd9861..4c4084a 100755 --- a/setup.py +++ b/setup.py @@ -1,12 +1,17 @@ #!/usr/bin/env python +#-*- coding:utf-8 -*- from distutils.core import setup -setup(name='pynest', - version='1.0', - description='Python API for Nest Thermostat', +setup(name='nest_thermostat', + version='1.1', + description='Python API and command line tool for talking to the Nestâ„¢ Thermostat', author='Scott Baker', author_email='smbaker@gmail.com', - url='http://www.smbaker.com/', + maintainer='Filippo Valsorda', + maintainer_email='hi@filippo.io', + url='https://github.com/FiloSottile/nest_thermostat/', scripts=['nest.py'], - ) + packages=['nest_thermostat'], + install_requires = ['requests'] +)