Skip to content

Instantly share code, notes, and snippets.

@harperreed
Created September 30, 2012 21:54
Show Gist options
  • Save harperreed/3808559 to your computer and use it in GitHub Desktop.
Save harperreed/3808559 to your computer and use it in GitHub Desktop.
Nest python class
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)`
#! /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