Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Multiple Structure and Device support #12

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ test/version_tmp
tmp
.env
vendor/bundle
**/.DS_Store
41 changes: 25 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,35 @@ Or install it yourself as:
Get some useful info:
```ruby
@nest = NestThermostat::Nest.new({email: ENV['NEST_EMAIL'], password: ENV['NEST_PASS']})
puts @nest.current_temperature # => 75.00
puts @nest.current_temp # => 75.00
puts @nest.temperature # => 73.00
puts @nest.temp # => 73.00
puts @nest.target_temperature_at # => 2012-06-05 14:28:48 +0000 # Ruby date object or false
puts @nest.target_temp_at # => 2012-06-05 14:28:48 +0000 # Ruby date object or false
puts @nest.away # => false
puts @nest.leaf # => true # May take a few seconds after a temp change
puts @nest.humidity # => 54 # Relative humidity in percent
@nest.structures # => Array of all Structures NestThermostat::Nest::Structure
@nest.devices # => Array of all devices from all structures NestThermostat::Nest::Device
@device = @nest.devices.first
@structure = @nest.structures.first
puts @device.current_temperature # => 75.00
puts @device.current_temp # => 75.00
puts @device.temperature # => 73.00
puts @device.temp # => 73.00
puts @device.target_temperature_at # => 2012-06-05 14:28:48 +0000 # Ruby date object or false
puts @device.target_temp_at # => 2012-06-05 14:28:48 +0000 # Ruby date object or false
puts @structure.away # => false
puts @device.leaf # => true # May take a few seconds after a temp change
puts @device.humidity # => 54 # Relative humidity in percent
```

Change the temperature or away status:
```ruby
puts @nest.temperature # => 73.0
puts @nest.temperature = 74.0
puts @nest.temperature # => 74.0

puts @nest.away # => false
puts @nest.away = true
puts @nest.away # => true
# @nest changes for all structures and all devices if applicable
# @structure changes for all devices of the structure if applicable

puts @device.temperature # => 73.0
puts @device.temperature = 74.0
@nest.refresh
puts @device.temperature # => 74.0

puts @structure.away # => false
puts @structure.away = true
@nest.refresh
puts @structure.away # => true
```

Default temperatures are in fahrenheit but you can change to celsius or kelvin:
Expand Down
163 changes: 62 additions & 101 deletions lib/nest_thermostat/nest.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require 'nest_thermostat/nest/structure'

require 'rubygems'
require 'httparty'
require 'json'
Expand All @@ -7,7 +9,7 @@ module NestThermostat
class Nest
attr_accessor :email, :password, :login_url, :user_agent, :auth,
:temperature_scale, :login, :token, :user_id, :transport_url,
:transport_host, :structure_id, :device_id, :headers
:transport_host, :structures, :headers, :auto_refresh

def initialize(config = {})
raise 'Please specify your nest email' unless config[:email]
Expand All @@ -19,103 +21,72 @@ def initialize(config = {})
self.temperature_scale = config[:temperature_scale] || config[:temp_scale] || 'f'
self.login_url = config[:login_url] || 'https://home.nest.com/user/login'
self.user_agent = config[:user_agent] ||'Nest/1.1.0.10 CFNetwork/548.0.4'
self.auto_refresh = config[:auto_refresh] || false
self.structures = []

# Login and get token, user_id and URLs
perform_login
self.token = @auth["access_token"]
self.user_id = @auth["userid"]
self.transport_url = @auth["urls"]["transport_url"]
self.transport_host = URI.parse(self.transport_url).host
self.headers = {
'Host' => self.transport_host,
'User-Agent' => self.user_agent,
'Authorization' => 'Basic ' + self.token,
'X-nl-user-id' => self.user_id,
'X-nl-protocol-version' => '1',
'Accept-Language' => 'en-us',
'Connection' => 'keep-alive',
'Accept' => '*/*'
}
set_session_variables

# Set device and structure id
status
# Set devices and structures
@status = get_status(true)
end

def status
request = HTTParty.get("#{self.transport_url}/v2/mobile/user.#{self.user_id}", headers: self.headers) rescue nil
result = JSON.parse(request.body) rescue nil

self.structure_id = result['user'][user_id]['structures'][0].split('.')[1]
self.device_id = result['structure'][structure_id]['devices'][0].split('.')[1]

result
def devices
self.structures.collect(&:devices).flatten
end

def public_ip
status["track"][self.device_id]["last_ip"].strip
def auth
@auth ||= perform_login
end

def leaf
status["device"][self.device_id]["leaf"]
def temp_scale=(scale)
self.temperature_scale = scale
end

def humidity
status["device"][self.device_id]["current_humidity"]
def status
self.auto_refresh ? get_status(false) : @status ||= get_status(false)
end

def current_temperature
convert_temp_for_get(status["shared"][self.device_id]["current_temperature"])
def refresh
set_session_variables && (@status = get_status(true))
end
alias_method :current_temp, :current_temperature

def temperature
convert_temp_for_get(status["shared"][self.device_id]["target_temperature"])
def away=(state)
structures.collect{|s| s.away = state }
end
alias_method :temp, :temperature

def temperature=(degrees)
degrees = convert_temp_for_set(degrees)

request = HTTParty.post(
"#{self.transport_url}/v2/put/shared.#{self.device_id}",
body: %Q({"target_change_pending":true,"target_temperature":#{degrees}}),
headers: self.headers
) rescue nil
structures.collect{|s| s.temperature = degrees}
end
alias_method :temp=, :temperature=

def target_temperature_at
epoch = status["device"][self.device_id]["time_to_target"]
epoch != 0 ? Time.at(epoch) : false
end
alias_method :target_temp_at, :target_temperature_at

def away
status["structure"][structure_id]["away"]
def fan_mode=(state)
structures.collect{|s| s.fan_mode = state}
end

def away=(state)
def set_control_for(control_type, item, message)
request = HTTParty.post(
"#{self.transport_url}/v2/put/structure.#{self.structure_id}",
body: %Q({"away_timestamp":#{Time.now.to_i},"away":#{!!state},"away_setter":0}),
"#{self.transport_url}/v2/put/#{control_type}.#{item.id}",
body: message,
headers: self.headers
) rescue nil
end

def temp_scale=(scale)
self.temperature_scale = scale
end
def status_for(status_type, item = nil)
item ? self.status[status_type][item.id] : self.status[status_type]

def fan_mode
status["device"][self.device_id]["fan_mode"]
end

def fan_mode=(state)
HTTParty.post(
"#{self.transport_url}/v2/put/device.#{self.device_id}",
body: %Q({"fan_mode":"#{state}"}),
headers: self.headers
) rescue nil
def method_missing(method, *args, &block)
#set_control_for
if control_type = method.to_s.match(/set_control_for_(.*)?/)
self.set_control_for(control_type[1], *args)
elsif status_type = method.to_s.match(/status_for_(.*)?/)
self.status_for(status_type[1], *args)
else
super
end
end

private
Expand All @@ -125,47 +96,37 @@ def perform_login
body: { username: self.email, password: self.password },
headers: { 'User-Agent' => self.user_agent }
)

self.auth ||= JSON.parse(login_request.body) rescue nil
raise 'Invalid login credentials' if self.auth.has_key?('error') && self.auth['error'] == "access_denied"
end

def convert_temp_for_get(degrees)
case self.temperature_scale
when /[fF](ahrenheit)?/
c2f(degrees).round(3)
when /[kK](elvin)?/
c2k(degrees).round(3)
else
degrees
end
end

def convert_temp_for_set(degrees)
case self.temperature_scale
when /[fF](ahrenheit)?/
f2c(degrees).round(5)
when /[kK](elvin)?/
k2c(degrees).round(5)
else
degrees
end
auth_response = JSON.parse(login_request.body) rescue nil
raise 'Invalid login credentials' if auth_response.has_key?('error') && auth_response['error'] == "access_denied"
return auth_response
end

def k2c(degrees)
degrees.to_f - 273.15
def set_session_variables
self.token = self.auth["access_token"]
self.user_id = self.auth["userid"]
self.transport_url = self.auth["urls"]["transport_url"]
self.transport_host = URI.parse(self.transport_url).host
self.headers = {
'Host' => self.transport_host,
'User-Agent' => self.user_agent,
'Authorization' => 'Basic ' + self.token,
'X-nl-user-id' => self.user_id,
'X-nl-protocol-version' => '1',
'Accept-Language' => 'en-us',
'Connection' => 'keep-alive',
'Accept' => '*/*'
}
end

def c2k(degrees)
degrees.to_f + 273.15
end
def get_status(reload_structures = true)
request = HTTParty.get("#{self.transport_url}/v2/mobile/user.#{self.user_id}", headers: self.headers) rescue nil
result = JSON.parse(request.body) rescue nil

def c2f(degrees)
degrees.to_f * 9.0 / 5 + 32
end
if reload_structures
self.structures = result['structure'].collect{|structure_id, config| Structure.new(self, structure_id, config)}
end

def f2c(degrees)
(degrees.to_f - 32) * 5 / 9
result
end
end
end
106 changes: 106 additions & 0 deletions lib/nest_thermostat/nest/device.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
module NestThermostat
class Nest
class Device
attr_accessor :structure, :id

def initialize(structure, id, config = {})
self.structure = structure
self.id = id
end

def public_ip
nest.status["track"][self.id]["last_ip"].strip
end

def leaf
status["leaf"]
end

def humidity
status["current_humidity"]
end

def current_temperature
convert_temp_for_get(shared_status["current_temperature"])
end
alias_method :current_temp, :current_temperature

def temperature
convert_temp_for_get(shared_status["target_temperature"])
end
alias_method :temp, :temperature

def temperature=(degrees)
degrees = convert_temp_for_set(degrees)
nest.set_control_for_shared(self, %Q({"target_change_pending":true,"target_temperature":#{degrees}}))
end
alias_method :temp=, :temperature=

def target_temperature_at
epoch = status["time_to_target"]
epoch != 0 ? Time.at(epoch) : false
end
alias_method :target_temp_at, :target_temperature_at

def fan_mode
status["fan_mode"]
end

def fan_mode=(state)
nest.set_control_for_device(self, %Q({"fan_mode":"#{state}"}))
end

private

def nest
structure.nest
end

def status
nest.status_for_device(self)
end

def shared_status
nest.status_for_shared(self)
end

def convert_temp_for_get(degrees)
case nest.temperature_scale
when /[fF](ahrenheit)?/
c2f(degrees).round(3)
when /[kK](elvin)?/
c2k(degrees).round(3)
else
degrees
end
end

def convert_temp_for_set(degrees)
case nest.temperature_scale
when /[fF](ahrenheit)?/
f2c(degrees).round(5)
when /[kK](elvin)?/
k2c(degrees).round(5)
else
degrees
end
end

def k2c(degrees)
degrees.to_f - 273.15
end

def c2k(degrees)
degrees.to_f + 273.15
end

def c2f(degrees)
degrees.to_f * 9.0 / 5 + 32
end

def f2c(degrees)
(degrees.to_f - 32) * 5 / 9
end
end
end
end
Loading