From b9a08bb25d31259a1277759d827bf2bb84aff90a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 12 Jan 2015 23:31:31 -0800 Subject: [PATCH] Migrate nest platform to python-nest --- homeassistant/__init__.py | 1 + homeassistant/components/thermostat/nest.py | 52 +-- homeassistant/external/pynest/nest.py | 365 -------------------- requirements.txt | 3 + 4 files changed, 34 insertions(+), 387 deletions(-) delete mode 100644 homeassistant/external/pynest/nest.py diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index e889282fae4..0ed4b2b2fd2 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -552,6 +552,7 @@ class StateMachine(object): not be affected. """ + new_state = str(new_state) attributes = attributes or {} with self._lock: diff --git a/homeassistant/components/thermostat/nest.py b/homeassistant/components/thermostat/nest.py index 38522b73e72..2802599dbc0 100644 --- a/homeassistant/components/thermostat/nest.py +++ b/homeassistant/components/thermostat/nest.py @@ -22,69 +22,77 @@ def get_devices(hass, config): return [] try: - # pylint: disable=no-name-in-module, unused-variable - import homeassistant.external.pynest.nest as pynest # noqa + import nest except ImportError: - logger.exception("Error while importing dependency phue.") + logger.exception( + "Error while importing dependency nest. " + "Did you maybe not install the python-nest dependency?") return [] - return [NestThermostat(username, password)] + napi = nest.Nest(username, password) + + return [ + NestThermostat(structure, device) + for structure in napi.structures + for device in structure.devices] class NestThermostat(ThermostatDevice): """ Represents a Nest thermostat within Home Assistant. """ - def __init__(self, username, password): - # pylint: disable=no-name-in-module, import-error - import homeassistant.external.pynest.nest as pynest - - self.nest = pynest.Nest(username, password) - self.nest.login() - self.update() + def __init__(self, structure, device): + self.structure = structure + self.device = device @property def name(self): """ Returns the name of the nest, if any. """ - return "Nest" + return self.device.name @property def unit_of_measurement(self): """ Returns the unit of measurement. """ - return TEMP_FAHRENHEIT if self.nest.units == 'F' else TEMP_CELCIUS + return TEMP_CELCIUS @property def device_state_attributes(self): """ Returns device specific state attributes. """ - return None + # Move these to Thermostat Device and make them global + return { + "humidity": self.device.humidity, + "target_humidity": self.device.target_humidity, + "fan": self.device.fan, + "mode": self.device.mode + } @property def current_temperature(self): """ Returns the current temperature. """ - return self.nest.get_curtemp() + return round(self.device.temperature, 1) @property def target_temperature(self): """ Returns the temperature we try to reach. """ - return self.nest.get_tartemp() + return round(self.device.target, 1) @property def is_away_mode_on(self): """ Returns if away mode is on. """ - return self.nest.is_away() + return self.structure.away def set_temperature(self, temperature): """ Set new target temperature """ - self.nest.set_temperature(temperature) + self.device.target = temperature def turn_away_mode_on(self): """ Turns away on. """ - self.nest.set_away("away") + self.structure.away = True def turn_away_mode_off(self): """ Turns away off. """ - self.nest.set_away("here") + self.structure.away = False def update(self): - """ Update nest. """ - self.nest.get_status() + """ Python-nest has its own mechanism for staying up to date. """ + pass diff --git a/homeassistant/external/pynest/nest.py b/homeassistant/external/pynest/nest.py deleted file mode 100644 index 081aed10a85..00000000000 --- a/homeassistant/external/pynest/nest.py +++ /dev/null @@ -1,365 +0,0 @@ -#! /usr/bin/python - -# nest.py -- a python interface to the Nest Thermostat -# by Scott M Baker, smbaker@gmail.com, http://www.smbaker.com/ -# -# Adapted to Python 3 by Stefano Fiorini -# -# Usage: -# 'nest.py help' will tell you what to do and how to do it -# -# Licensing: -# This is distributed under the Creative Commons 3.0 Non-commercial, -# 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 time -import codecs -import urllib.request, urllib.parse, urllib.error -import urllib.request, urllib.error, urllib.parse -import sys -import re -import ssl -import http.client, socket -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 simplejson. Python 2.6+ contains json library already.") - sys.exit(-1) - -#force connection to be TLSv1 -class HTTPSConnectionV1(http.client.HTTPSConnection): - def __init__(self, *args, **kwargs): - http.client.HTTPSConnection.__init__(self, *args, **kwargs) - - def connect(self): - sock = socket.create_connection((self.host, self.port), self.timeout) - self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, ssl_version=ssl.PROTOCOL_TLSv1) - -class HTTPSHandlerV1(urllib.request.HTTPSHandler): - def https_open(self, req): - return self.do_open(HTTPSConnectionV1, req) -# install opener -urllib.request.install_opener(urllib.request.build_opener(HTTPSHandlerV1())) - -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 - self.headers={"user-agent":"Nest/1.1.0.10 CFNetwork/548.0.4", - "X-nl-protocol-version": "1"} - def loads2(self, res): - binary_data = res.decode("utf-8") - return json.loads(binary_data) - - def loads(self, res): - reader = codecs.getreader("utf-8") - if hasattr(json, "loads"): - res = json.loads(reader(res)) - else: - res = json.read(reader(res)) - return res - - # context ['shared','structure','device'] - def handle_put(self, context, data): - assert context is not None, "Context must be set to ['shared','structure','device']" - assert data is not None, "Data is None" - - new_url = self.transport_url + "/v2/put/" + context + "." - - if (context == "shared" or context == "device"): - new_url += self.serial - elif (context == "structure"): - new_url += self.structure_id - else: - raise ValueError(context+ " is unsupported") - - binary_data = data.encode("utf-8") - req = urllib.request.Request(new_url, binary_data, self.headers) - - try: - urllib.request.urlopen(req).read() - except urllib.error.URLError: - print ("Put operation failed") - if (self.debug): - print (new_url) - print (data) - - def shared_put(self, data): - self.handle_put("shared", data) - - def device_put(self, data): - self.handle_put("device", data) - - def structure_put(self, data): - self.handle_put("structure", data) - - def login(self): - data = urllib.parse.urlencode({"username": self.username, "password": self.password}) - - binary_data = data.encode("utf-8") - req = urllib.request.Request("https://home.nest.com/user/login", - binary_data, self.headers) - - res = urllib.request.urlopen(req).read() - - res = self.loads2(res) - - self.transport_url = res["urls"]["transport_url"] - self.userid = res["userid"] - self.headers["Authorization"] = "Basic " + res["access_token"] - self.headers["X-nl-user-id"]= self.userid - - def get_status(self): - req = urllib.request.Request(self.transport_url + "/v2/mobile/user." + self.userid, - headers=self.headers) - - res = urllib.request.urlopen(req).read() - - res = self.loads2(res) - - 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] - structure = self.status["structure"][self.structure_id] - - # Delete the structure name so that we preserve the device name - del structure["name"] - allvars = shared - - allvars.update(structure) - allvars.update(device) - - for k, v in sorted(allvars.items()): - print((k + "."*(32-len(k)) + ":", self.format_value(k, v))) - - def format_value(self, key, value): - if 'temp' in key and isinstance(value, float) and self.units == 'F': - return '%s (%s F)' % (value, self.temp_out(value)) - - elif 'timestamp' in key or key == 'creation_time': - if value > 0xffffffff: - value /= 1000 - return time.ctime(value) - - elif key == 'mac_address' and len(value) == 12: - return ':'.join(value[i:i+2] for i in range(0, 12, 2)) - - else: - return str(value) - - def get_units(self): - return self.units - - def get_tartemp(self): - temp = self.status["shared"][self.serial]["target_temperature"] - temp = self.temp_out(temp) - temp = ("%0.0f" % temp) - - return temp - - def get_curtemp(self): - temp = self.status["shared"][self.serial]["current_temperature"] - temp = self.temp_out(temp) - temp = ("%0.1f" % temp) - - return temp - - def show_curtemp(self): - print(self.get_curtemp()) - - def is_away(self): - return self.status["structure"][self.structure_id]["away"] - - def set_temperature(self, temp): - temp = self.temp_in(temp) - data = '{"target_change_pending":true,"target_temperature":' + '%0.1f' % temp + '}' - self.shared_put(data) - - def set_fan(self, state): - data = '{"fan_mode":"' + str(state) + '"}' - self.device_put(data) - - def set_mode(self, state): - data = '{"target_temperature_type":"' + str(state) + '"}' - self.shared_put(data) - - def set_away(self, state): - time_since_epoch = time.time() - if (state == "away"): - data = '{"away_timestamp":' + str(time_since_epoch) + ',"away":true,"away_setter":0}' - else: - data = '{"away_timestamp":' + str(time_since_epoch) + ',"away":false,"away_setter":0}' - - self.structure_put(data) - - def set_auto_away(self, state): - if (state == "enable"): - data = '{"auto_away_enable":true}' - else: - data = '{"auto_away_enable":false}' - self.device_put(data) - -def create_parser(): - parser = OptionParser(usage="nest [options] command [command_options] [command_args]", - description="Commands: fan temp mode away auto-away", - 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("-d", "--debug", dest="debug", action="store_true", default=False, - help="Print debug information") - - 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, away, mode, show, curtemp, curhumid") - print (" temp ... set target temperature") - print (" fan [auto|on] ... set fan state") - print (" away [away|here] ... set away state") - print (" auto-away [enable|disable]... enable or disable auto away") - print (" mode [heat|cool|range] ... set thermostat mode") - 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 validate_temp(temp): - try: - new_temp = float(temp) - except ValueError: - return -1 - if new_temp < 50 or new_temp > 90: - return -1 - return new_temp - -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, debug=opts.debug) - n.login() - n.get_status() - - cmd = args[0] - - if (cmd == "temp"): - new_temp = -1 - if len(args)>1: - new_temp = validate_temp(args[1]) - if new_temp == -1: - print ("please specify a temperature between 50 and 90") - sys.exit(-1) - n.set_temperature(new_temp) - elif (cmd == "fan"): - if len(args)<2 or args[1] not in {"on", "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 or args[1] not in {"cool", "heat", "range"}: - print ("please specify a thermostat mode of 'cool', 'heat' or 'range'") - sys.exit(-1) - n.set_mode(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"])) - elif (cmd == "away"): - if len(args)<2 or args[1] not in {"away", "here"}: - print ("please specify a state of 'away' or 'here'") - sys.exit(-1) - n.set_away(args[1]) - elif (cmd == "auto-away"): - if len(args)<2 or args[1] not in {"enable", "disable"}: - print ("please specify a state of 'enable' or 'disable'") - sys.exit(-1) - n.set_auto_away(args[1]) - else: - print(("misunderstood command:", cmd)) - print ("do 'nest.py help' for help") - -if __name__=="__main__": - main() diff --git a/requirements.txt b/requirements.txt index e0e003dd5eb..4bf9fc83d6a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,3 +26,6 @@ python-libnmap # notify.pushbullet pushbullet.py>=0.7.1 + +# thermostat.nest +python-nest>=2.1