nest_connect 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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