nest_connect 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,66 @@
1
+ module NestConnect
2
+ class ChunkParser
3
+ EVENT = -'event: '
4
+ DATA = -'data: '
5
+
6
+ def self.write(chunk)
7
+ unless chunk.empty?
8
+ new(chunk)
9
+ end
10
+ end
11
+
12
+ def initialize(chunk)
13
+ @raw_event_line, @raw_data_line = chunk.split("\n")
14
+ end
15
+
16
+ def event
17
+ event_line
18
+ end
19
+
20
+ def data
21
+ JSON.parse(data_line, symbolize_names: true) || {}
22
+ end
23
+
24
+ def thermostats
25
+ Device::Thermostat.from_hash_collection(thermostats_hash)
26
+ end
27
+
28
+ def protects
29
+ Device::Protect.from_hash_collection(smoke_co_alarms_hash)
30
+ end
31
+
32
+ def cameras
33
+ Device::Camera.from_hash_collection(cameras_hash)
34
+ end
35
+
36
+ private
37
+
38
+ def data_hash
39
+ data.fetch(:data, {})
40
+ end
41
+
42
+ def devices_hash
43
+ data_hash.fetch(:devices, {})
44
+ end
45
+
46
+ def thermostats_hash
47
+ devices_hash.fetch(:thermostats, {})
48
+ end
49
+
50
+ def smoke_co_alarms_hash
51
+ devices_hash.fetch(:smoke_co_alarms, {})
52
+ end
53
+
54
+ def cameras_hash
55
+ devices_hash.fetch(:cameras, {})
56
+ end
57
+
58
+ def data_line
59
+ @raw_data_line.gsub(DATA, "")
60
+ end
61
+
62
+ def event_line
63
+ @raw_event_line.gsub(EVENT, "")
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,53 @@
1
+ require 'yaml'
2
+
3
+ module NestConnect
4
+ class ConfigStore
5
+ def initialize(path)
6
+ @path = path
7
+ end
8
+
9
+ def [](key)
10
+ data[key]
11
+ end
12
+
13
+ def save(key, value)
14
+ data[key] = value
15
+
16
+ find_or_create_directory
17
+ find_or_create_file
18
+ persist_data
19
+
20
+ value
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :path
26
+
27
+ def find_or_create_directory
28
+ Dir.mkdir(pathname.dirname) unless Dir.exist?(pathname.dirname)
29
+ end
30
+
31
+ def find_or_create_file
32
+ File.new(path, "w") unless File.exists?(path)
33
+ end
34
+
35
+ def persist_data
36
+ File.open(path, "w") { |f| f.write(data.to_yaml) }
37
+ end
38
+
39
+ def pathname
40
+ Pathname.new(path)
41
+ end
42
+
43
+ def data
44
+ @_data ||= load_data
45
+ end
46
+
47
+ def load_data
48
+ YAML.load(File.read(path)) || {}
49
+ rescue
50
+ {}
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,60 @@
1
+ module NestConnect
2
+ class Device
3
+ class Camera
4
+ def self.from_hash_collection(hash)
5
+ hash.values.map { |value| new(value) }
6
+ end
7
+
8
+ def initialize(api_class: NestConnect::API::Devices::Camera, **args)
9
+ @api_class = api_class
10
+ args.each do |key, value|
11
+ instance_variable_set("@#{key}", value)
12
+ end
13
+ end
14
+
15
+ def reload
16
+ api_runner.get.body.each do |key, value|
17
+ instance_variable_set("@#{key}", value)
18
+ end
19
+ end
20
+
21
+ attr_reader(
22
+ :device_id,
23
+ :software_version,
24
+ :structure_id,
25
+ :where_id,
26
+ :where_name,
27
+ :name,
28
+ :name_long,
29
+ :is_online,
30
+ :is_audio_input_enabled,
31
+ :last_is_online_change,
32
+ :is_video_history_enabled,
33
+ :web_url,
34
+ :app_url,
35
+ :is_public_share_enabled,
36
+ :activity_zones,
37
+ :public_share_url,
38
+ :snapshot_url,
39
+ :last_event
40
+ )
41
+
42
+ attr_reader :is_streaming
43
+
44
+ def is_streaming=(value)
45
+ normalized_value = !!value
46
+
47
+ api_runner.put({is_streaming: normalized_value})
48
+ @is_streaming = normalized_value
49
+ end
50
+
51
+ private
52
+
53
+ attr_reader :api_class
54
+
55
+ def api_runner
56
+ api_class.new(device_id)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,49 @@
1
+ module NestConnect
2
+ class Device
3
+ class Protect
4
+ def self.from_hash_collection(hash)
5
+ hash.values.map { |value| new(value) }
6
+ end
7
+
8
+ def initialize(api_class: NestConnect::API::Devices::Protect, **args)
9
+ @api_class = api_class
10
+ args.each do |key, value|
11
+ instance_variable_set("@#{key}", value)
12
+ end
13
+ end
14
+
15
+ def reload
16
+ api_runner.get.body.each do |key, value|
17
+ instance_variable_set("@#{key}", value)
18
+ end
19
+ end
20
+
21
+ attr_reader(
22
+ :battery_health,
23
+ :co_alarm_state,
24
+ :device_id,
25
+ :is_manual_test_active,
26
+ :is_online,
27
+ :last_connection,
28
+ :last_manual_test_time,
29
+ :locale,
30
+ :name,
31
+ :name_long,
32
+ :smoke_alarm_state,
33
+ :software_version,
34
+ :structure_id,
35
+ :ui_color_state,
36
+ :where_id,
37
+ :where_name
38
+ )
39
+
40
+ private
41
+
42
+ attr_reader :api_class
43
+
44
+ def api_runner
45
+ api_class.new(device_id)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,85 @@
1
+ module NestConnect
2
+ class Device
3
+ class Structure
4
+ def self.from_hash_collection(hash)
5
+ hash.values.map { |value| new(value) }
6
+ end
7
+
8
+ def initialize(api_class: NestConnect::API::Devices::Structure, **args)
9
+ @api_class = api_class
10
+ args.each do |key, value|
11
+ instance_variable_set("@#{key}", value)
12
+ end
13
+ end
14
+
15
+ def reload
16
+ api_runner.get.body.each do |key, value|
17
+ instance_variable_set("@#{key}", value)
18
+ end
19
+ end
20
+
21
+ attr_reader(
22
+ :co_alarm_state,
23
+ :country_code,
24
+ :eta_begin,
25
+ :peak_period_end_time,
26
+ :peak_period_start_time,
27
+ :postal_code,
28
+ :rhr_enrollment,
29
+ :smoke_alarm_state,
30
+ :structure_id,
31
+ :time_zone,
32
+ :wheres,
33
+ :wwn_security_state
34
+ )
35
+
36
+ def thermostats
37
+ @thermostats.to_a.map do |device_id|
38
+ Device::Thermostat.new(device_id: device_id)
39
+ end
40
+ end
41
+
42
+ def protects
43
+ @smoke_alarm_state.to_a.map do |device_id|
44
+ Device::Protect.new(device_id: device_id)
45
+ end
46
+ end
47
+
48
+ def cameras
49
+ @cameras.to_a.map do |device_id|
50
+ Device::Camera.new(device_id: device_id)
51
+ end
52
+ end
53
+
54
+ AWAY_VALUES = ['home', 'away']
55
+
56
+ attr_reader :away
57
+
58
+ def away=(value)
59
+ unless AWAY_VALUES.include?(value)
60
+ raise ValueError.new("away must be #{AWAY_VALUES}")
61
+ end
62
+
63
+ api_runner.put({away: value})
64
+ @away = value
65
+ end
66
+
67
+ attr_reader :name
68
+
69
+ def name=(value)
70
+ normalized_value = value.to_s
71
+
72
+ api_runner.put({name: normalized_value})
73
+ @name = normalized_value
74
+ end
75
+
76
+ private
77
+
78
+ attr_reader :api_class
79
+
80
+ def api_runner
81
+ api_class.new(structure_id)
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,194 @@
1
+ module NestConnect
2
+ class RangeError < StandardError; end
3
+ class ValueError < StandardError; end
4
+
5
+ class Device
6
+ class Thermostat
7
+ def self.from_hash_collection(hash)
8
+ hash.values.map { |value| new(value) }
9
+ end
10
+
11
+ def initialize(api_class: NestConnect::API::Devices::Thermostat, **args)
12
+ @api_class = api_class
13
+ args.each do |key, value|
14
+ instance_variable_set("@#{key}", value)
15
+ end
16
+ end
17
+
18
+ def reload
19
+ api_runner.get.body.each do |key, value|
20
+ instance_variable_set("@#{key}", value)
21
+ end
22
+ end
23
+
24
+ TARGET_TEMPERATURE_F_RANGE = (50..90)
25
+
26
+ attr_reader :target_temperature_f
27
+
28
+ def target_temperature_f=(value)
29
+ normalized_value = value.round
30
+
31
+ unless TARGET_TEMPERATURE_F_RANGE.include?(normalized_value)
32
+ raise RangeError.new("target_temperature_f must be between #{TARGET_TEMPERATURE_F_RANGE}")
33
+ end
34
+
35
+ api_runner.put({target_temperature_f: normalized_value})
36
+ @target_temperature_f = normalized_value
37
+ end
38
+
39
+ TARGET_TEMPERATURE_C_RANGE = (9..30)
40
+
41
+ attr_reader :target_temperature_c
42
+
43
+ def target_temperature_c=(value)
44
+ normalized_value = (value * 2).round / 2.0
45
+
46
+ unless TARGET_TEMPERATURE_C_RANGE.include?(normalized_value)
47
+ raise RangeError.new("target_temperature_c must be between #{TARGET_TEMPERATURE_C_RANGE}")
48
+ end
49
+
50
+ api_runner.put({target_temperature_c: normalized_value})
51
+ @target_temperature_c = normalized_value
52
+ end
53
+
54
+ attr_reader :fan_timer_active
55
+
56
+ def fan_timer_active=(value)
57
+ normalized_value = !!value
58
+
59
+ api_runner.put({fan_timer_active: normalized_value})
60
+ @fan_timer_active = normalized_value
61
+ end
62
+
63
+ FAN_TIMER_DURATION_VALUES = [15, 30, 45, 60, 120, 240, 480, 720]
64
+
65
+ attr_reader :fan_timer_duration
66
+
67
+ def fan_timer_duration=(value)
68
+ unless FAN_TIMER_DURATION_VALUES.include?(value)
69
+ raise ValueError.new("fan_timer_duration must be #{FAN_TIMER_DURATION_VALUES}")
70
+ end
71
+
72
+ api_runner.put({fan_timer_duration: value})
73
+ @fan_timer_duration = value
74
+ end
75
+
76
+ HVAC_MODE_VALUES = ['heat', 'cool', 'heat-cool', 'eco', 'off']
77
+
78
+ attr_reader :hvac_mode
79
+
80
+ def hvac_mode=(value)
81
+ unless HVAC_MODE_VALUES.include?(value)
82
+ raise ValueError.new("hvac_mode must be #{HVAC_MODE_VALUES}")
83
+ end
84
+
85
+ api_runner.put({hvac_mode: value})
86
+ @hvac_mode = value
87
+ end
88
+
89
+ attr_reader :label
90
+
91
+ def label=(value)
92
+ normalized_value = value.to_s
93
+
94
+ api_runner.put({label: normalized_value})
95
+ @label = normalized_value
96
+ end
97
+
98
+ attr_reader :target_temperature_high_c
99
+
100
+ def target_temperature_high_c=(value)
101
+ normalized_value = (value * 2).round / 2.0
102
+
103
+ api_runner.put({target_temperature_high_c: normalized_value})
104
+ @target_temperature_high_c = normalized_value
105
+ end
106
+
107
+ attr_reader :target_temperature_low_c
108
+
109
+ def target_temperature_low_c=(value)
110
+ normalized_value = (value * 2).round / 2.0
111
+
112
+ api_runner.put({target_temperature_low_c: normalized_value})
113
+ @target_temperature_low_c = normalized_value
114
+ end
115
+
116
+ attr_reader :target_temperature_high_f
117
+
118
+ def target_temperature_high_f=(value)
119
+ normalized_value = value.round
120
+
121
+ api_runner.put({target_temperature_high_f: normalized_value})
122
+ @target_temperature_high_f = normalized_value
123
+ end
124
+
125
+ attr_reader :target_temperature_low_f
126
+
127
+ def target_temperature_low_f=(value)
128
+ normalized_value = value.round
129
+
130
+ api_runner.put({target_temperature_low_f: normalized_value})
131
+ @target_temperature_low_f = normalized_value
132
+ end
133
+
134
+ TEMPERATURE_SCALE_VALUES = ['C', 'F']
135
+
136
+ attr_reader :temperature_scale
137
+
138
+ def temperature_scale=(value)
139
+ unless TEMPERATURE_SCALE_VALUES.include?(value)
140
+ raise ValueError.new("temperature_scale must be #{TEMPERATURE_SCALE_VALUES}")
141
+ end
142
+
143
+ api_runner.put({temperature_scale: value})
144
+ @temperature_scale = value
145
+ end
146
+
147
+ attr_reader(
148
+ :ambient_temperature_c,
149
+ :ambient_temperature_f,
150
+ :can_cool,
151
+ :can_heat,
152
+ :device_id,
153
+ :eco_temperature_high_c,
154
+ :eco_temperature_high_f,
155
+ :eco_temperature_low_c,
156
+ :eco_temperature_low_f,
157
+ :fan_timer_duration,
158
+ :fan_timer_timeout,
159
+ :has_fan,
160
+ :has_leaf,
161
+ :humidity,
162
+ :hvac_state,
163
+ :is_locked,
164
+ :is_online,
165
+ :is_using_emergency_heat,
166
+ :last_connection,
167
+ :locale,
168
+ :locked_temp_max_c,
169
+ :locked_temp_max_f,
170
+ :locked_temp_min_c,
171
+ :locked_temp_min_f,
172
+ :name,
173
+ :name_long,
174
+ :previous_hvac_mode,
175
+ :software_version,
176
+ :structure_id,
177
+ :sunlight_correction_active,
178
+ :sunlight_correction_enabled,
179
+ :time_to_target,
180
+ :time_to_target_training,
181
+ :where_id,
182
+ :where_name
183
+ )
184
+
185
+ private
186
+
187
+ attr_reader :api_class
188
+
189
+ def api_runner
190
+ api_class.new(device_id)
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,37 @@
1
+ module NestConnect
2
+ class GlobalConfig
3
+ DEFAULT_PATH = ENV["HOME"] + '/.nest_connect/config'
4
+
5
+ unless defined? @@path
6
+ @@path = nil
7
+ end
8
+
9
+ def self.path=(new_path)
10
+ @@path = new_path
11
+ end
12
+
13
+ def self.path
14
+ @@path || DEFAULT_PATH
15
+ end
16
+
17
+ def initialize(store = nil)
18
+ @store = store || ConfigStore.new(self.class.path)
19
+ end
20
+
21
+ def access_token
22
+ @_access_token ||= store[:access_token] || configure_access_token
23
+ end
24
+
25
+ def access_token=(token)
26
+ store.save(:access_token, token)
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :store
32
+
33
+ def configure_access_token
34
+ raise 'please configure your access token first by running nest_connect authorize'
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,3 @@
1
+ module NestConnect
2
+ VERSION = "0.1.1"
3
+ end
@@ -0,0 +1,14 @@
1
+ require 'json'
2
+
3
+ module NestConnect
4
+ end
5
+
6
+ require 'nest_connect/version'
7
+ require 'nest_connect/global_config'
8
+ require 'nest_connect/config_store'
9
+ require 'nest_connect/api/api'
10
+ require 'nest_connect/chunk_parser'
11
+ require 'nest_connect/devices/camera'
12
+ require 'nest_connect/devices/protect'
13
+ require 'nest_connect/devices/structure'
14
+ require 'nest_connect/devices/thermostat'
@@ -0,0 +1,33 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "nest_connect/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "nest_connect"
8
+ spec.version = NestConnect::VERSION
9
+ spec.authors = ["Karl Entwistle"]
10
+ spec.email = ["karl@entwistle.com"]
11
+
12
+ spec.summary = %q{Simple API Wrapper for Nest Thermostats}
13
+ spec.homepage = "https://github.com/karlentwistle/nest_connect"
14
+
15
+ # Specify which files should be added to the gem when it is released.
16
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
17
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
18
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_dependency 'faraday', '~> 0.15'
25
+ spec.add_dependency 'faraday_middleware', '~> 0.12'
26
+ spec.add_dependency 'thor', '~> 0.20'
27
+
28
+ spec.add_development_dependency "bundler", "~> 1.17"
29
+ spec.add_development_dependency "byebug"
30
+ spec.add_development_dependency "rake", "~> 10.0"
31
+ spec.add_development_dependency "rspec", "~> 3.0"
32
+ spec.add_development_dependency "webmock"
33
+ end