Skip to content

Instantly share code, notes, and snippets.

@ironicbadger
Last active January 31, 2019 06:57
Show Gist options
  • Select an option

  • Save ironicbadger/cd67af6843ef33604a047b88b0bd4b2f to your computer and use it in GitHub Desktop.

Select an option

Save ironicbadger/cd67af6843ef33604a047b88b0bd4b2f to your computer and use it in GitHub Desktop.

nest_poller

A simple Ruby daemon that reads from the Nest thermostat API (using your personal Nest login credentials), collects some basic stats, and publishes those stats to an instance of InfluxDB somewhere on the Internet. InfluxDB is hooked up to Grafana for visualization in my case.

This is a quick hack for my own personal use and is completely unsupported.

#!/usr/bin/env ruby
# USAGE
# See : https://github.com/thuehlinger/daemons
#
# ruby nest_poller_control.rb run (foreground)
# ruby nest_poller_control.rb start
# ruby nest_poller_control.rb stop
# ruby nest_poller_control.rb status
require 'rubygems'
require 'bundler/setup'
require 'daemons'
require 'httparty'
require 'influxdb'
require 'awesome_print'
Bundler.setup
# Instructions for getting a long-life access token for API access
# for rempe.us registered API client.
#
# Guide:
# https://developer.nest.com/documentation/cloud/rest-quick-guide
#
# See : https://developer.nest.com/clients for my registered API clients and the URLs.
#
# Step 1 : GET a PIN number
# - Replace STATE with a random number.
# - Paste this in a BROWSER as you will have to click a button to authorize.
# - A PIN code will be returned which you will need for the next step.
#
# GET https://home.nest.com/login/oauth2?client_id=3fb1f2d0-1f87-42f5-a9f8-0b21f142aef5&state=STATE
#
# Step 2 : Copy PIN code.
# e.g. N6B4FV9G
#
# Step 3 : Exchange the authorization PIN code for a long-lived access token
#
# Replace code=AUTHORIZATION_CODE in this URL with the PIN
#
# POST https://api.home.nest.com/oauth2/access_token?client_id=3fb1f2d0-1f87-42f5-a9f8-0b21f142aef5&code=AUTHORIZATION_CODE&client_secret=D1AVcTSab5ERaolisto5uUDIc&grant_type=authorization_code
#
# Step 4 : Extract long-life access token from response which will be used for all future API requests.
#
# {"access_token":"c.7MVm2...","expires_in":315360000}
NEST_WEATHER_URL = "http://weather.nest.com/weather/v1?query="
Daemons.run_proc('nest_poller.rb', {:backtrace => true, :monitor => true, :log_output => true}) do
# influxdb setup
database = 'nest_poller'
time_precision = 'm'
influxdb = InfluxDB::Client.new(database,
host: ENV['INFLUXDB_NEST_POLLER_HOST'],
username: ENV['INFLUXDB_NEST_POLLER_USERNAME'],
password: ENV['INFLUXDB_NEST_POLLER_PASS'])
loop do
# influxdb data collection bucket
data = []
begin
api_req = HTTParty.get("https://developer-api.nest.com/?auth=#{ENV['NEST_API_ACCESS_TOKEN']}")
api_resp = JSON.parse(api_req.body) rescue nil
rescue Exception => e
puts "api HTTP req failed : #{e.class} #{e.message}"
api_resp = nil
end
# report the weather for each home structure on the account.
if api_resp && api_resp["structures"] && api_resp["structures"] && api_resp["structures"].is_a?(Hash)
# Iterate over all structures to get the outdoor weather for it.
api_resp["structures"].keys.each do |id|
postal_code = api_resp["structures"][id]["postal_code"]
# sample : http://weather.nest.com/weather/v1?query=11746
outdoor_weather_req = HTTParty.get("#{NEST_WEATHER_URL}#{postal_code}")
outdoor_weather_resp = JSON.parse(outdoor_weather_req.body) rescue nil
if outdoor_weather_resp && outdoor_weather_resp[postal_code] && outdoor_weather_resp[postal_code]["current"]
temp_f = outdoor_weather_resp[postal_code]["current"]["temp_f"]
condition = outdoor_weather_resp[postal_code]["current"]["condition"]
humidity = outdoor_weather_resp[postal_code]["current"]["humidity"]
weather = {}
weather[:series] = 'weather'
weather[:values] = {temp_f: temp_f, condition: condition, humidity: humidity }
weather[:tags] = {postal_code: postal_code}
data << weather
end
end
else
puts "api_resp['structures'] check failed. : #{api_resp}"
end
# Iterate over each thermostat and collect the data for it.
if api_resp && api_resp["devices"] && api_resp["devices"]["thermostats"] && api_resp["devices"]["thermostats"].is_a?(Hash)
api_resp["devices"]["thermostats"].keys.each do |id|
d = api_resp["devices"]["thermostats"][id]
device = {}
device[:series] = 'nest'
device[:values] = {}
device[:values][:indoor_temp_f] = d["ambient_temperature_f"]
device[:values][:indoor_temp_setpoint] = d["target_temperature_f"]
device[:values][:indoor_humidity] = d["humidity"]
device[:tags] = { device_type: 'nest',
structure_id: d["structure_id"],
structure_name: api_resp["structures"][d["structure_id"]]["name"],
device_id: d["device_id"],
device_location: d["name"],
postal_code: api_resp["structures"][d["structure_id"]]["postal_code"]}
data << device
end
else
puts "api_resp['devices'] check failed. : #{api_resp}"
end
# write all collected data for structures and thermostats
begin
influxdb.write_points(data, time_precision)
rescue Exception => e
puts "influxdb.write_points req failed : #{e.class} : #{e.message}"
# could not write data points to InfluxDB server. Try again soon.
end
sleep(60 * 5) # seconds
end
end
@paradizelost

Copy link
Copy Markdown

I'm trying to use a container that uses this code, and it looks like something may be broken with the zip code lookup. it's returning blank data, causing the data to not be written to influx. once i hard-coded a zip instead of using the api call in the 2 locations, i was able to get data into influx without error. the error i was getting is:
influxdb.write_points req failed : InfluxDB::Error : {"error":"unable to parse 'nest,device_type=nest,structure_id=MYID,structure_name=Home,device_id=DEVICEID,device_location=LivingRoom,postal_code= indoor_temp_f=69,indoor_temp_setpoint=68,indoor_humidity=25': missing tag value"}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment