Created
September 30, 2012 21:54
-
-
Save harperreed/3808559 to your computer and use it in GitHub Desktop.
Nest python class
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os | |
from NestThermostat import NestThermostat | |
if __name__ == "__main__": | |
""" | |
A simple test for the nest class | |
You will need to either define environment variables below or set the variables in the script: | |
NEST_USERNAME | |
NEST_PASSWORD | |
NEST_NAME | |
NEST_LOCATION | |
""" | |
nest_username = '' #nest.com username (usually email address) | |
nest_password = '' #nest.com password | |
nest_name = '' #The name of the Nest you want to control (as entered on nest.com) | |
nest_location = '' #The location of the Nest you want to control (as entered on nest.com) | |
if not nest_username: | |
nest_username = os.environ['NEST_USERNAME'] | |
if not nest_password: | |
nest_password = os.environ['NEST_PASSWORD'] | |
if not nest_name: | |
nest_name = os.environ['NEST_NAME'] | |
if not nest_location: | |
nest_location = os.environ['NEST_LOCATION'] | |
nest = NestThermostat(username=nest_username,password=nest_password,name=nest_name,location=nest_location) | |
print "Current temperature: " + str(nest.get_temp())+ "" + nest.get_temp_scale() | |
print "Current humidity: " + str(nest.get_humidity()) | |
print "Fan mode: " +nest.get_fan_mode() | |
print "Target temperature: " + str(nest.get_target_temp())+ "" + nest.get_temp_scale() | |
print "Mode: "+ nest.get_heat_cool_mode() | |
print "Is the fan on: "+ ("Yes" if nest.fan_is_on() else "No") | |
print "Is the heat on: "+ ("Yes" if nest.heat_is_on() else "No") | |
print "Is the A/C on: "+ ("Yes" if nest.ac_is_on() else "No") | |
print "Is the nest set to away: "+ ("Yes" if nest.away_is_active() else "No") | |
#print nest.set_target_temp(74.0)` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /usr/bin/env python | |
# -*- coding: utf-8 -*- | |
#################### | |
# from | |
# https://github.com/johnray/Indigo-Nest-Thermostat-Plugin | |
# Copyright (c) 2012, Perceptive Automation, LLC. All rights reserved. | |
# http://www.perceptiveautomation.com | |
import os | |
import sys | |
import random | |
import urllib2 | |
import urllib | |
import time | |
# Need json support; Use "simplejson" for Indigo support | |
try: | |
import simplejson as json | |
except: | |
import json | |
# Time limit for the cache to exist between requiring an update | |
NEST_CACHE_REFRESH_TIMEOUT=5 | |
# Time limit between auth token refreshes | |
NEST_AUTH_REFRESH_TIMEOUT=3600 | |
# Maximum number of retries before deciding that sending a command failed | |
NEST_MAX_RETRIES=5 | |
# Time to wait between retries (in seconds) | |
NEST_RETRY_WAIT=0.1 | |
# Simple constant mapping for fan, heat/cool type, etc. | |
NEST_FAN_MAP={'auto on':"auto",'on': "on", 'auto': "auto", 'always on': "on", '1': "on", '0': "auto"} | |
NEST_AWAY_MAP={'on':True,'away':True,'off':False,'home':False,True:True, False:False} | |
NEST_HEAT_COOL_MAP={'cool':'cool','cooling':'cool','heat':'heat','heating': | |
'heat','range':'range','both':'range','auto':"range",'off':'off'} | |
# Nest URL Constants. These shouldn't be changed. | |
NEST_URLS="urls" | |
NEST_TRANSPORT_URL="transport_url" | |
NEST_LOGIN_URL="https://home.nest.com/user/login" | |
NEST_STATUS_URL_FRAGMENT="/v2/mobile/user." | |
NEST_SHARED_URL_FRAGMENT="/v2/put/shared." | |
NEST_DEVICE_URL_FRAGMENT="/v2/put/device." | |
NEST_STRUCTURE_URL_FRAGMENT="/v2/put/structure." | |
# Nest Data Constants. These shouldn't be changed. | |
NEST_USER_ID="userid" | |
NEST_ACCESS_TOKEN="access_token" | |
NEST_DEVICE_DATA="device" | |
NEST_SHARED_DATA="shared" | |
NEST_STRUCTURE_DATA="structure" | |
NEST_STRUCTURE_NAME="name" | |
NEST_DEVICE_NAME="name" | |
# Nest Status Constants. These shouldn't be changed, but if the module is expanded, | |
# new constants can be placed here. | |
NEST_CURRENT_TEMP="current_temperature" | |
NEST_CURRENT_HUMIDITY="current_humidity" | |
NEST_CURRENT_FAN_MODE="fan_mode" | |
NEST_TARGET_TEMP="target_temperature" | |
NEST_TARGET_CHANGE_PENDING="target_change_pending" | |
NEST_HEAT_COOL_MODE="target_temperature_type" | |
NEST_RANGE_TEMP_HIGH="target_temperature_high" | |
NEST_RANGE_TEMP_LOW="target_temperature_low" | |
NEST_HEAT_ON="hvac_heater_state" | |
NEST_AC_ON="hvac_ac_state" | |
NEST_FAN_ON="hvac_fan_state" | |
NEST_TEMP_SCALE="temperature_scale" | |
NEST_AWAY="away" | |
class NestThermostat: | |
def __init__(self, username, password, name, location): | |
"""Initialize a new Nest thermostat object | |
Arguments: | |
username - username for Nest website | |
password - password for Nest website | |
name - The name of the Nest you want to control (as entered on nest.com) | |
location - The location of the Nest you want to control (as entered on nest.com) | |
""" | |
self._username=username | |
self._password=password | |
self._nest_name=name | |
self._structure_name=location | |
self._refresh_auth() | |
self._refresh_status() | |
def _refresh_auth(self): | |
"""Refreshes the Nest login token. | |
The Nest site authentication token expires after a set period of time. This | |
method refreshes it. All methods in this class automatically call this method | |
after NEST_AUTH_REFRESH_TIMEOUT seconds have passed, so calling it explicitly | |
is unneeded. | |
""" | |
send_data=urllib.urlencode({"username":self._username,"password":self._password}) | |
init_data=json.loads((urllib2.urlopen(urllib2.Request(NEST_LOGIN_URL,send_data))).read()) | |
# Store time of refresh of the auth token | |
self._last_auth_refresh=time.time() | |
# Pieces needed for status and control | |
self._transport_url=init_data[NEST_URLS][NEST_TRANSPORT_URL] | |
access_token=init_data[NEST_ACCESS_TOKEN] | |
user_id=init_data[NEST_USER_ID] | |
# Setup the header and status URL that will be needed elsewhere in the class | |
self._header={"Authorization":"Basic "+access_token,"X-nl-protocol-version": "1"} | |
self._status_url=self._transport_url+NEST_STATUS_URL_FRAGMENT+user_id | |
# Invalidate the cache | |
self._cached=False | |
def _refresh_status(self): | |
"""Refreshes the Nest thermostat data. | |
This method grabs the current data from the Nest website for use with the | |
rest of the class methods. If NEST_AUTH_REFRESH_TIMEOUT seconds haven't yet | |
passed, the method doesn't do anything (ie. the existing data remains cached). | |
This method is called automatically by other methods that return information from | |
the Nest, so calling it explicitly is unneeded. | |
""" | |
# Before doing anything, check to see if the auth token should be refreshed | |
if ((time.time()-self._last_auth_refresh)>NEST_AUTH_REFRESH_TIMEOUT): | |
self._refresh_auth() | |
# Refresh the status data, if needed | |
if (not self._cached or (time.time()-self._last_update>NEST_CACHE_REFRESH_TIMEOUT)): | |
self._cached=True | |
self._last_update=time.time() | |
self._status_data=json.loads((urllib2.urlopen(urllib2.Request(self._status_url,headers=self._header))).read()) | |
# Loop through structures to find the named structure and build a lookup table | |
structures=self._status_data[NEST_STRUCTURE_DATA] | |
self._nest_structures=dict() | |
for key in structures.keys(): | |
self._nest_structures[structures[key][NEST_STRUCTURE_NAME].lower()]=key | |
# Look through serial numbers to find Nest names and build a lookup table | |
serials=self._status_data[NEST_SHARED_DATA] | |
self._nest_serials=dict() | |
for key in serials.keys(): | |
self._nest_serials[serials[key][NEST_DEVICE_NAME].lower()]=key | |
# Use this to set the serial and structure (location) instance variables and construct the URLs. | |
# I'd rather do this earlier, but letting the user refer to the Nest (and its location) by name | |
# is worth it. | |
self._serial=self._nest_serials[self._nest_name.lower()] | |
self._structure=self._nest_structures[self._structure_name.lower()] | |
# Setup the remaining URLs for the class | |
self._shared_url=self._transport_url+NEST_SHARED_URL_FRAGMENT+self._serial | |
self._device_url=self._transport_url+NEST_DEVICE_URL_FRAGMENT+self._serial | |
self._structure_url=self._transport_url+NEST_STRUCTURE_URL_FRAGMENT+self._structure | |
def _get_attribute(self,attribute): | |
"""Returns the value of a Nest thermostat attribute, such as the current temperature. | |
Arguments: | |
attribute - The attribute string to retrieve | |
""" | |
try: | |
return self._status_data[NEST_DEVICE_DATA][self._serial][attribute] | |
except: | |
try: | |
return self._status_data[NEST_SHARED_DATA][self._serial][attribute] | |
except: | |
return self._status_data[NEST_STRUCTURE_DATA][self._structure][attribute] | |
def _apply_temp_scale(self,temp): | |
"""Given a temperature, returns the temperature in F or C depending on the Nest's settings. | |
This method is used for getting the appropriate temperature reading when retrieving settings | |
from the Nest. | |
For sending temperatures values, use _apply_temp_scale_c() to convert them (if | |
needed) to C. | |
Arguments: | |
temp - The temperature (float) to convert (if needed) | |
""" | |
if (self.temp_scale_is_f()): | |
return round(temp*1.8+32) | |
else: | |
return round(temp) | |
def _send_command(self,command,url): | |
"""Attempts to send a command to the Nest thermostat via the Nest website. | |
This method accepts a command string (JSON data) and attempts to send it to the Nest | |
site. If the transmission fails, it fails silently since error checking must be handled | |
by validating that a change has been made in the Nest attributes. | |
Arguments: | |
command - JSON formatted data to send to the Nest site | |
url - The URL where the data should be posted | |
""" | |
discard_me="" | |
try: | |
discard_me=urllib2.urlopen(urllib2.Request(url,command,headers=self._header)).read() | |
except: | |
# Do nothing | |
pass | |
return discard_me | |
def _apply_temp_scale_c(self,temp): | |
"""Given a temperature, returns the temperature in C, if needed depending on the Nest's settings. | |
This method is used when sending data to the Nest. The Nest expects values to be sent in C | |
regardless of its internal temperature scale setting. This method is used by the class to | |
automatically convert temperatures (when sending) to C if needed. | |
For reading temperatures values, use _apply_temp_scale_() to convert them (if needed) to F. | |
Arguments: | |
temp - The temperature (float) to convert (if needed) | |
""" | |
if (self.temp_scale_is_f()): | |
return (temp-32)/1.8 | |
else: | |
return temp | |
def get_temp(self): | |
"""Returns the current temperature (float) reported by the Nest.""" | |
# Update the current status | |
self._refresh_status() | |
return self._apply_temp_scale(self._get_attribute(NEST_CURRENT_TEMP)) | |
def get_humidity(self): | |
"""Returns the current humidity (integer representing percentage) reported by the Nest.""" | |
# Update the current status | |
self._refresh_status() | |
return round(self._get_attribute(NEST_CURRENT_HUMIDITY)) | |
def get_fan_mode(self): | |
"""Returns 'auto' if the fan turns on automatically, or 'on' if it is always on.""" | |
# Update the current status | |
self._refresh_status() | |
return (NEST_FAN_MAP[self._get_attribute(NEST_CURRENT_FAN_MODE)]) | |
def get_target_temp(self): | |
"""Returns the temperature (float) that the Nest is trying to reach.""" | |
# Update the current status | |
self._refresh_status() | |
return self._apply_temp_scale(self._get_attribute(NEST_TARGET_TEMP)) | |
def target_temp_change_is_pending(self): | |
"""Returns True if the Nest is trying to set a new target temperature.""" | |
# Update the current status, this is time sensitive so invalidate the cache | |
self._cached=False | |
self._refresh_status() | |
return self._get_attribute(NEST_TARGET_CHANGE_PENDING) | |
def get_temp_scale(self): | |
"""Returns 'F' if the Nest is set to Farenheit, 'C' if Celcius.""" | |
# Get temperature scale (F or C) from Nest | |
self._refresh_status() | |
return self._get_attribute(NEST_TEMP_SCALE) | |
def get_range_temps(self): | |
"""Returns a dictionary with the 'high' and 'low' temperatures (float) set for the Nest. | |
The range temperatures are only used when the nest is in auto heat/cool mode. The | |
dictionary keys for the method, in case it wasn't obvious, are 'high' for the upper | |
temperature limit (how hot can it get), and 'low' for the low limit (how cold). | |
""" | |
# Update the current status | |
self._refresh_status() | |
return {'low':self._apply_temp_scale(self._get_attribute(NEST_RANGE_TEMP_LOW)), | |
'high':self._apply_temp_scale(self._get_attribute(NEST_RANGE_TEMP_HIGH))} | |
def get_heat_cool_mode(self): | |
"""Returns 'cool' when Nest in AC mode, 'heat' in heating mode, and 'auto' in heat/cool mode. | |
Returns a string that identifies the mode that the Nest is operating in. AC is 'cool', | |
heating is 'heat', and maintaining a temperature range is 'auto'. If the system is off, 'off' | |
is returned. | |
Note that the value returned is passed through a dictionary so it can be mapped to | |
alternative strings. This was included for ease of integration with Indigo and can just | |
be ignored for general use. | |
""" | |
# Update the current status | |
self._refresh_status() | |
return NEST_HEAT_COOL_MAP[self._get_attribute(NEST_HEAT_COOL_MODE)] | |
def temp_scale_is_f(self): | |
"""Returns True if the Nest temperature scale is Fahrenheit, False if Celcius""" | |
if (self.get_temp_scale()=="F"): | |
return True | |
else: | |
return False | |
def fan_is_on(self): | |
"""Returns True if the fan is currently on.""" | |
# Update the current status | |
self._refresh_status() | |
return self._get_attribute(NEST_FAN_ON) | |
def heat_is_on(self): | |
"""Returns True if the heat is currently on.""" | |
# Update the current status | |
self._refresh_status() | |
return self._get_attribute(NEST_HEAT_ON) | |
def ac_is_on(self): | |
"""Returns True if the AC is currently on.""" | |
# Update the current status | |
self._refresh_status() | |
return self._get_attribute(NEST_AC_ON) | |
def away_is_active(self): | |
"""Returns True if the Nest is in 'away' mode.""" | |
# Update the current status | |
self._refresh_status() | |
return self._get_attribute(NEST_AWAY) | |
def set_fan_mode(self,command='auto'): | |
"""Sets the Nest fan mode to 'on' (always on) or 'auto' based on the provided command string. | |
Arguments: | |
command - A string representing the Nest fan mode. 'on' for always on, 'auto' for auto. | |
Note that the value sent to the Nest is passed through a dictionary so it can be mapped to | |
alternative strings. This was included for ease of integration with Indigo and can just | |
be ignored for general use. | |
""" | |
self._refresh_status() | |
send_data=json.dumps({NEST_CURRENT_FAN_MODE:NEST_FAN_MAP[command]}) | |
retry_count=0 | |
while (retry_count<NEST_MAX_RETRIES): | |
self._cached=False | |
self._send_command(send_data,self._device_url) | |
retry_count=retry_count+1 | |
if (NEST_FAN_MAP[command]==self.get_fan_mode()): | |
return True | |
time.sleep(NEST_RETRY_WAIT) | |
return False | |
def set_away_state(self,command='off'): | |
"""Sets the Nest away state 'on' (away) or 'off' (home) based on the provided command string. | |
Arguments: | |
command - A string representing the away state. 'on' for away, 'off' for home. | |
Note that the value sent to the Nest is passed through a dictionary so it can be mapped to | |
alternative strings. In this case, I liked 'on' and 'off' better than true or false. | |
""" | |
self._refresh_status() | |
send_data=json.dumps({NEST_AWAY:NEST_AWAY_MAP[command]}) | |
retry_count=0 | |
while (retry_count<NEST_MAX_RETRIES): | |
self._cached=False | |
self._send_command(send_data,self._structure_url) | |
retry_count=retry_count+1 | |
if ((NEST_AWAY_MAP[command]==True and self.away_is_active()) or | |
(NEST_AWAY_MAP[command]==False and not self.away_is_active())): | |
return True | |
time.sleep(NEST_RETRY_WAIT) | |
return False | |
def set_heat_cool_mode(self,command='cool'): | |
"""Sets the Nest thermostat mode to 'cool' (AC), 'heat' (heating), 'range' (auto heat/cool), or 'off'. | |
Arguments: | |
command - A string representing the Nest heat/cool mode. 'cool' for AC, 'heat' for heating, | |
'range' for maintaining a temperature range, or 'off' to turn the HVAC system off. | |
Default is 'cool' because I hate the heat. | |
Note that the value sent to the Nest is passed through a dictionary so it can be mapped to | |
alternative strings. This was included for ease of integration with Indigo and can just | |
be ignored for general use. | |
""" | |
self._refresh_status() | |
send_data=json.dumps({NEST_HEAT_COOL_MODE:NEST_HEAT_COOL_MAP[command]}) | |
retry_count=0 | |
while (retry_count<NEST_MAX_RETRIES): | |
self._cached=False | |
self._send_command(send_data,self._shared_url) | |
retry_count=retry_count+1 | |
if (NEST_HEAT_COOL_MAP[command]==self.get_heat_cool_mode()): | |
return True | |
time.sleep(NEST_RETRY_WAIT) | |
return False | |
def set_range_temps(self,low_temp,high_temp): | |
"""Sets the high and low temperatures to be maintained by the Nest when in 'range' heat/cool mode. | |
Arguments: | |
low_temp - The lowest (coldest) temperature to allow before heating kicks in. | |
high_temp - The highest (hottest) temperature allowed before cooling kicks in. | |
""" | |
self._refresh_status() | |
send_data=json.dumps({NEST_RANGE_TEMP_LOW:self._apply_temp_scale_c(low_temp), | |
NEST_RANGE_TEMP_HIGH:self._apply_temp_scale_c(high_temp)}) | |
retry_count=0 | |
while (retry_count<NEST_MAX_RETRIES): | |
self._cached=False | |
self._send_command(send_data,self._shared_url) | |
retry_count=retry_count+1 | |
range_temps=self.get_range_temps() | |
if (round(range_temps['low'])==round(low_temp) and round(range_temps['high'])==round(high_temp)): | |
return True | |
time.sleep(NEST_RETRY_WAIT) | |
return False | |
def set_target_temp(self,new_temp): | |
"""Sets a new target temperature on the Nest. This is the same as turning physical Nest dial. | |
Arguments: | |
new_temp - The temperature the Nest will try to reach and maintain. | |
""" | |
self._refresh_status() | |
send_data=json.dumps({NEST_TARGET_TEMP:self._apply_temp_scale_c(new_temp),NEST_TARGET_CHANGE_PENDING:True}) | |
retry_count=0 | |
while (retry_count<NEST_MAX_RETRIES or self.target_temp_change_is_pending()): | |
self._cached=False | |
self._send_command(send_data,self._shared_url) | |
retry_count=retry_count+1 | |
if (round(new_temp)==round(self.get_target_temp())): | |
return True | |
time.sleep(NEST_RETRY_WAIT) | |
return False |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment