alexa_hue 1.0.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b9fe3c650ed88c2c5cf706a1877915c87b97ecc1
4
+ data.tar.gz: 48b1157a1a2a04ab60b4f2c45fbda7621e873098
5
+ SHA512:
6
+ metadata.gz: e77f71eb3efe6ecd76f1d2caaff0c90a3a76f43489ed9c6b19b0b8a6ec7d6ba38b2a7c84b4e17bf4b1c2c6cb6b29929368b99ea56c50bb791d93e1b37c9cb63f
7
+ data.tar.gz: b90184d7747efcfce1f23f72d37d7749783609eb303ce93345e7528770437e6eb8edc0c439c8840580cf96399115e5fb8cf00244ecdad74129cbf684792cf57b
data/.DS_Store ADDED
Binary file
data/.gitignore ADDED
@@ -0,0 +1,139 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ InstalledFiles
7
+ _yardoc
8
+ coverage
9
+ doc/
10
+ lib/bundler/man
11
+ pkg
12
+ rdoc
13
+ spec/reports
14
+ test/tmp
15
+ test/version_tmp
16
+ tmp
17
+ *.bundle
18
+ *.so
19
+ *.o
20
+ *.a
21
+ mkmf.log
22
+
23
+ # Created by https://www.gitignore.io
24
+
25
+ ### RubyMine ###
26
+ # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm
27
+
28
+ *.iml
29
+
30
+ ## Directory-based project format:
31
+ .idea/
32
+ # if you remove the above rule, at least ignore the following:
33
+
34
+ # User-specific stuff:
35
+ # .idea/workspace.xml
36
+ # .idea/tasks.xml
37
+ # .idea/dictionaries
38
+
39
+ # Sensitive or high-churn files:
40
+ # .idea/dataSources.ids
41
+ # .idea/dataSources.xml
42
+ # .idea/sqlDataSources.xml
43
+ # .idea/dynamic.xml
44
+ # .idea/uiDesigner.xml
45
+
46
+ # Gradle:
47
+ # .idea/gradle.xml
48
+ # .idea/libraries
49
+
50
+ # Mongo Explorer plugin:
51
+ # .idea/mongoSettings.xml
52
+
53
+ ## File-based project format:
54
+ *.ipr
55
+ *.iws
56
+
57
+ ## Plugin-specific files:
58
+
59
+ # IntelliJ
60
+ /out/
61
+
62
+ # mpeltonen/sbt-idea plugin
63
+ .idea_modules/
64
+
65
+ # JIRA plugin
66
+ atlassian-ide-plugin.xml
67
+
68
+ # Crashlytics plugin (for Android Studio and IntelliJ)
69
+ com_crashlytics_export_strings.xml
70
+ crashlytics.properties
71
+ crashlytics-build.properties
72
+
73
+
74
+ ### SublimeText ###
75
+ # cache files for sublime text
76
+ *.tmlanguage.cache
77
+ *.tmPreferences.cache
78
+ *.stTheme.cache
79
+
80
+ # workspace files are user-specific
81
+ *.sublime-workspace
82
+
83
+ # project files should be checked into the repository, unless a significant
84
+ # proportion of contributors will probably not be using SublimeText
85
+ # *.sublime-project
86
+
87
+ # sftp configuration file
88
+ sftp-config.json
89
+
90
+
91
+ ### Vim ###
92
+ [._]*.s[a-w][a-z]
93
+ [._]s[a-w][a-z]
94
+ *.un~
95
+ Session.vim
96
+ .netrwhist
97
+ *~
98
+
99
+
100
+ ### Ruby ###
101
+ *.gem
102
+ *.rbc
103
+ /.config
104
+ /coverage/
105
+ /InstalledFiles
106
+ /pkg/
107
+ /spec/reports/
108
+ /test/tmp/
109
+ /test/version_tmp/
110
+ /tmp/
111
+
112
+ ## Specific to RubyMotion:
113
+ .dat*
114
+ .repl_history
115
+ build/
116
+
117
+ ## Documentation cache and generated files:
118
+ /.yardoc/
119
+ /_yardoc/
120
+ /doc/
121
+ /rdoc/
122
+
123
+ ## Environment normalisation:
124
+ /.bundle/
125
+ /vendor/bundle
126
+ /lib/bundler/man/
127
+
128
+ # for a library or gem, you might want to ignore these files since the code is
129
+ # intended to run in multiple environments; otherwise, check them in:
130
+ # Gemfile.lock
131
+ # .ruby-version
132
+ # .ruby-gemset
133
+
134
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
135
+ .rvmrc
136
+
137
+ .idea
138
+
139
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,20 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ gem 'chronic', '~> 0.10.0'
6
+ gem 'chronic_duration', '~> 0'
7
+ gem 'httparty', '~> 0.13.0'
8
+ gem 'numbers_in_words', '~> 0.2.0'
9
+ gem 'sinatra'
10
+ gem 'alexa_objects'
11
+ gem 'activesupport'
12
+
13
+ group :development do
14
+ gem "guard", "2.12.5", require: false
15
+ gem "guard-rspec", '4.5.0', require: false
16
+ gem "guard-bundler", require: false
17
+ gem "guard-rake", require: false
18
+ gem "terminal-notifier-guard", require: false
19
+ gem 'guard-yard', require: false
20
+ end
data/Guardfile ADDED
File without changes
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/alexa_hue.gemspec ADDED
@@ -0,0 +1,37 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'alexa_hue/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "alexa_hue"
8
+ spec.version = Sinatra::Hue::VERSION
9
+ spec.authors = ["Kyle Lucas"]
10
+ spec.email = ["kglucas93@gmail.com"]
11
+ spec.summary = %q{A sinatra middleware for alexa hue actions.}
12
+ spec.description = %q{}
13
+ spec.homepage = "http://github.com/kylegrantlucas/alexa_hue"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = '>= 1.9.3'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0")
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ["lib", 'skills_config']
21
+
22
+ spec.add_runtime_dependency 'json'
23
+ spec.add_runtime_dependency 'alexa_objects'
24
+ spec.add_runtime_dependency 'httparty'
25
+ spec.add_runtime_dependency 'numbers_in_words'
26
+ spec.add_runtime_dependency 'activesupport'
27
+ spec.add_runtime_dependency 'chronic'
28
+ spec.add_runtime_dependency 'chronic_duration'
29
+ spec.add_runtime_dependency 'sinatra'
30
+
31
+ spec.add_development_dependency "bundler", "~> 1.6"
32
+ spec.add_development_dependency "rake"
33
+ spec.add_development_dependency "rspec"
34
+ spec.add_development_dependency "webmock"
35
+
36
+ spec.add_development_dependency "codeclimate-test-reporter"
37
+ end
@@ -0,0 +1,7 @@
1
+ module Sinatra
2
+ module Hue
3
+ def self.custom_slots
4
+ File.read(File.expand_path('../../../skills_config/custom_slots.txt', __FILE__))
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,19 @@
1
+ def fix_schedule_syntax(string)
2
+ sub_time = string.match(/time \d{2}:\d{2}/)
3
+ sub_duration = string.match(/schedule PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/)
4
+
5
+ if sub_time
6
+ sub_time = sub_time.to_s
7
+ string.slice!(sub_time).strip
8
+ string << " #{sub_time}"
9
+ string.sub!("time", "schedule at")
10
+ end
11
+
12
+ if sub_duration
13
+ sub_duration = sub_duration.to_s
14
+ string.slice!(sub_duration).strip
15
+ sub_duration = ChronicDuration.parse(sub_duration.split(' ').last)
16
+ string << " schedule in #{sub_duration} seconds"
17
+ end
18
+ string.strip if string
19
+ end
@@ -0,0 +1,522 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'socket'
4
+ require 'ipaddr'
5
+ require 'timeout'
6
+ require 'chronic'
7
+ require 'chronic_duration'
8
+ require 'httparty'
9
+ require 'numbers_in_words'
10
+ require 'numbers_in_words/duck_punch'
11
+ require 'timeout'
12
+
13
+ module Hue
14
+ module_function
15
+
16
+ def devices(options = {})
17
+ SSDP.new(options).devices
18
+ end
19
+
20
+ def first(options = {})
21
+ options = options.merge(:first => true)
22
+ SSDP.new(options).devices
23
+ end
24
+
25
+ class Switch
26
+ attr_accessor :command, :lights_array, :_group, :body, :schedule_params, :schedule_ids
27
+ def initialize(command = "", _group = 0, &block)
28
+
29
+ @user = "1234567890"
30
+ begin
31
+ @ip = HTTParty.get("https://www.meethue.com/api/nupnp").first["internalipaddress"] rescue nil
32
+ if @ip.nil?
33
+ bridge = get_bridge_by_SSDP
34
+ @ip = bridge.ip
35
+ end
36
+
37
+ rescue Timeout::Error
38
+ puts "Time Out"
39
+ rescue NoMethodError
40
+ puts "Cannot Find Bridge via Hue broker service, trying SSDP..."
41
+ rescue Errno::ECONNREFUSED
42
+ puts "Connection refused"
43
+ rescue SocketError
44
+ puts "Cannot connect to local network"
45
+ end
46
+
47
+ authorize_user
48
+ populate_switch
49
+
50
+ self.lights_array = []
51
+ self.schedule_ids = []
52
+ self.schedule_params = nil
53
+ self.command = ""
54
+ self._group = "0"
55
+ self.body = {}
56
+ instance_eval(&block) if block_given?
57
+ end
58
+
59
+ def list_lights
60
+ light_list = {}
61
+ HTTParty.get("http://#{@ip}/api/#{@user}/lights").each { |k,v| light_list["#{v['name']}".downcase] = k }
62
+ light_list
63
+ end
64
+
65
+ def list_groups
66
+ group_list = {}
67
+ HTTParty.get("http://#{@ip}/api/#{@user}/groups").each { |k,v| group_list["#{v['name']}".downcase] = k }
68
+ group_list["all"] = "0"
69
+ group_list
70
+ end
71
+
72
+ def list_scenes
73
+ scene_list = {}
74
+ HTTParty.get("http://#{@ip}/api/#{@user}/scenes").each do |scene|
75
+ if scene[1]["owner"] != "none"
76
+ inner = {"id" => scene[0]}.merge(scene[1])
77
+ scene_list.merge!({"#{scene[1]["name"].split(' ').first.downcase}" => inner})
78
+ end
79
+ end
80
+ scene_list
81
+ end
82
+
83
+ def hue (numeric_value)
84
+ clear_attributes
85
+ self.body[:hue] = numeric_value
86
+ end
87
+
88
+ def mired (numeric_value)
89
+ clear_attributes
90
+ self.body[:ct] = numeric_value
91
+ end
92
+
93
+ def color(color_name)
94
+ clear_attributes
95
+ if @colors.keys.include?(color_name.to_sym)
96
+ self.body[:hue] = @colors[color_name.to_sym]
97
+ else
98
+ self.body[:ct] = @mired_colors[color_name.to_sym]
99
+ end
100
+ end
101
+
102
+ def saturation(depth)
103
+ self.body.delete(:scene)
104
+ self.body[:sat] = depth
105
+ end
106
+
107
+ def brightness(depth)
108
+ self.body.delete(:scene)
109
+ self.body[:bri] = depth
110
+ end
111
+
112
+ def clear_attributes
113
+ self.body.delete(:scene)
114
+ self.body.delete(:ct)
115
+ self.body.delete(:hue)
116
+ end
117
+
118
+ def fade(in_seconds)
119
+ self.body[:transitiontime] = in_seconds * 10
120
+ end
121
+
122
+ def light (*args)
123
+ self.lights_array = []
124
+ self._group = ""
125
+ self.body.delete(:scene)
126
+ args.each { |l| self.lights_array.push @lights[l.to_s] if @lights.keys.include?(l.to_s) }
127
+ end
128
+
129
+ def lights(group_name)
130
+ self.lights_array = []
131
+ self.body.delete(:scene)
132
+ group = @groups[group_name.to_s]
133
+ self._group = group if !group.nil?
134
+ end
135
+
136
+ def scene(scene_name)
137
+ clear_attributes
138
+ scene_details = self.list_scenes[scene_name]
139
+ self.lights_array = scene_details["lights"]
140
+ self._group = "0"
141
+ self.body[:scene] = scene_details["id"]
142
+ end
143
+
144
+ def confirm
145
+ params = {:alert => 'select'}
146
+ HTTParty.put("http://#{@ip}/api/#{@user}/groups/0/action" , :body => params.to_json)
147
+ end
148
+
149
+ def save_scene(scene_name)
150
+ scene_name.gsub!(' ','-')
151
+ self.fade 2 if self.body[:transitiontime] == nil
152
+ if self._group.empty?
153
+ light_group = HTTParty.get("http://#{@ip}/api/#{@user}/groups/0")["lights"]
154
+ else
155
+ light_group = HTTParty.get("http://#{@ip}/api/#{@user}/groups/#{self._group}")["lights"]
156
+ end
157
+ params = {name: scene_name, lights: light_group, transitiontime: self.body[:transitiontime]}
158
+ response = HTTParty.put("http://#{@ip}/api/#{@user}/scenes/#{scene_name}", :body => params.to_json)
159
+ confirm if response.first.keys[0] == "success"
160
+ end
161
+
162
+ def lights_on_off
163
+ self.lights_array.each { |l| HTTParty.put("http://#{@ip}/api/#{@user}/lights/#{l}/state", :body => (self.body).to_json) }
164
+ end
165
+
166
+ def group_on_off
167
+ HTTParty.put("http://#{@ip}/api/#{@user}/groups/#{self._group}/action", :body => (self.body.reject { |s| s == :scene }).to_json)
168
+ end
169
+
170
+ def scene_on_off
171
+ if self.body[:on] == true
172
+ HTTParty.put("http://#{@ip}/api/#{@user}/groups/#{self._group}/action", :body => (self.body.select { |s| s == :scene }).to_json)
173
+ elsif self.body[:on] == false
174
+ # turn off individual lights in the scene
175
+ (HTTParty.get("http://#{@ip}/api/#{@user}/scenes"))[self.body[:scene]]["lights"].each do |l|
176
+ puts self.body
177
+ HTTParty.put("http://#{@ip}/api/#{@user}/lights/#{l}/state", :body => (self.body).to_json)
178
+ end
179
+ end
180
+ end
181
+
182
+ def on
183
+ self.body[:on]=true
184
+ lights_on_off if self.lights_array.any?
185
+ group_on_off if (!self._group.empty? && self.body[:scene].nil?)
186
+ scene_on_off if !self.body[:scene].nil?
187
+ end
188
+
189
+ def off
190
+ self.body[:on]=false
191
+ lights_on_off if self.lights_array.any?
192
+ group_on_off if (!self._group.empty? && self.body[:scene].nil?)
193
+ scene_on_off if !self.body[:scene].nil?
194
+ end
195
+
196
+ # Parses times in words (e.g., "eight forty five") to standard HH:MM format
197
+
198
+ def schedule (string, on_or_off = :default)
199
+ self.body[:on] = true if on_or_off == :on
200
+ self.body[:on] = false if on_or_off == :off
201
+ set_time = set_time(string)
202
+ if set_time < Time.now
203
+ p "You've scheduled this in the past"
204
+ else
205
+ set_time = set_time.to_s.split(' ')[0..1].join(' ').sub(' ',"T")
206
+ self.schedule_params = {:name=>"Hue_Switch Alarm",
207
+ :description=>"",
208
+ :localtime=>"#{set_time}",
209
+ :status=>"enabled",
210
+ :autodelete=>true
211
+ }
212
+ if self.lights_array.any?
213
+ lights_array.each do |l|
214
+ self.schedule_params[:command] = {:address=>"/api/#{@user}/lights/#{l}/state", :method=>"PUT", :body=>self.body}
215
+ end
216
+ else
217
+ self.schedule_params[:command] = {:address=>"/api/#{@user}/groups/#{self._group}/action", :method=>"PUT", :body=>self.body}
218
+ end
219
+ self.schedule_ids.push(HTTParty.post("http://#{@ip}/api/#{@user}/schedules", :body => (self.schedule_params).to_json))
220
+ confirm if self.schedule_ids.flatten.last.include?("success")
221
+ end
222
+ end
223
+
224
+ def delete_schedules!
225
+ self.schedule_ids.flatten!
226
+ self.schedule_ids.each { |k|
227
+ id = k["success"]["id"] if k.include?("success")
228
+ HTTParty.delete("http://#{@ip}/api/#{@user}/schedules/#{id}")
229
+ }
230
+ self.schedule_ids = []
231
+ end
232
+
233
+ def colorloop(start_or_stop)
234
+ if start_or_stop == :start
235
+ self.body[:effect] = "colorloop"
236
+ elsif start_or_stop == :stop
237
+ self.body[:effect] = "none"
238
+ end
239
+ end
240
+
241
+ def alert(value)
242
+ if value == :short
243
+ self.body[:alert] = "select"
244
+ elsif value == :long
245
+ self.body[:alert] = "lselect"
246
+ elsif value == :stop
247
+ self.body[:alert] = "none"
248
+ end
249
+ end
250
+
251
+ def reset
252
+ self.command = ""
253
+ self._group = "0"
254
+ self.body = {}
255
+ self.schedule_params = nil
256
+ end
257
+
258
+ #The following two methods are required to use Switch with Zach Feldman's Alexa-home*
259
+ def wake_words
260
+ ["light", "lights", "scene", "seen"]
261
+ end
262
+
263
+ def process_command (command)
264
+ command.sub!("color loop", "colorloop")
265
+ command.sub!("too", "two")
266
+ command.sub!("for", "four")
267
+ command.sub!(/a half$/, 'thirty seconds')
268
+ self.voice command
269
+ end
270
+
271
+ #The rest of the methods allow access to most of the Switch class functionality by supplying a single string
272
+
273
+ def voice(string)
274
+ puts string
275
+ self.reset
276
+ self.command << string
277
+
278
+ parse_voice(string)
279
+
280
+ if self.command.include?("schedule")
281
+ state = string.match(/off|on/)[0].to_sym rescue nil
282
+ self.send("schedule", *[string, state])
283
+ else
284
+ string.include?(' off') ? self.send("off") : self.send("on")
285
+ end
286
+ end
287
+
288
+ private
289
+
290
+ def parse_leading(methods)
291
+ methods.each do |l|
292
+ capture = (self.command.match (/\b#{l}\s\w+/)).to_s.split(' ')
293
+ method = capture[0]
294
+ value = capture[1]
295
+ value = value.in_numbers if value.scan(Regexp.union( (1..10).map {|k| k.in_words} ) ).any?
296
+ value = ((value.to_f/10.to_f)*255).to_i if (value.class == Fixnum) && (l != "fade")
297
+ self.send( method, value )
298
+ end
299
+ end
300
+
301
+ def parse_trailing(method)
302
+ all_keys = Regexp.union((@groups.keys + @lights.keys).flatten)
303
+ value = self.command.match(all_keys).to_s
304
+ self.send( method.first, value )
305
+ end
306
+
307
+ def parse_dynamic(methods)
308
+ methods.each do |d|
309
+ capture = (self.command.match (/\w+ #{d}\b/)).to_s.split(' ')
310
+ method = capture[1]
311
+ value = capture[0].to_sym
312
+ self.send( method, value )
313
+ end
314
+ end
315
+
316
+ def parse_scene(scene_name)
317
+ scene_name.gsub!(' ','-') if scene_name.size > 1
318
+ self.send("scene", scene_name)
319
+ end
320
+
321
+ def parse_save_scene
322
+ save_scene = self.command.partition(/save (scene|seen) as /).last
323
+ self.send( "save_scene", save_scene)
324
+ end
325
+
326
+ def parse_voice(string)
327
+ string.gsub!('schedule ','')
328
+ trailing = string.split(' ') & %w[lights light]
329
+ leading = string.split(' ') & %w[hue brightness saturation fade color]
330
+ dynamic = string.split(' ') & %w[colorloop alert]
331
+ scene_name = string.partition(" scene").first
332
+
333
+ parse_scene(scene_name) if string.include?(" scene") && !string.include?("save")
334
+ parse_leading(leading) if leading.any?
335
+ parse_trailing(trailing) if trailing.any?
336
+ parse_dynamic(dynamic) if dynamic.any?
337
+ parse_save_scene if self.command.scan(/save (scene|seen) as/).length > 0
338
+ end
339
+
340
+ def numbers_to_times(numbers)
341
+ numbers.map!(&:in_numbers)
342
+ numbers.map!(&:to_s)
343
+ numbers.push("0") if numbers[1] == nil
344
+ numbers = numbers.shift + ':' + (numbers[0].to_i + numbers[1].to_i).to_s
345
+ numbers.gsub!(':', ':0') if numbers.split(":")[1].length < 2
346
+ numbers
347
+ end
348
+
349
+ def parse_time(string)
350
+ string.sub!(" noon", " twelve in the afternoon")
351
+ string.sub!("midnight", "twelve in the morning")
352
+ time_modifier = string.downcase.scan(/(evening)|(night|tonight)|(afternoon)|(pm)|(a.m.)|(am)|(p.m.)|(morning)|(today)/).flatten.compact.first
353
+ guess = Time.now.strftime('%H').to_i >= 12 ? "p.m." : "a.m."
354
+ time_modifier = time_modifier.nil? ? guess : time_modifier
355
+ day_modifier = string.scan(/(tomorrow)|(next )?(monday|tuesday|wednesday|thursday|friday|saturday|sunday)/).flatten.compact.join(' ')
356
+ numbers_in_words = string.scan(Regexp.union((1..59).map(&:in_words)))
357
+ set_time = numbers_to_times(numbers_in_words)
358
+ set_time = Chronic.parse(day_modifier + ' ' + set_time + ' ' + time_modifier)
359
+ end
360
+
361
+ def set_time(string)
362
+ if string.scan(/ seconds?| minutes?| hours?| days?| weeks?/).any?
363
+ set_time = string.partition("in").last.strip!
364
+ set_time = Time.now + ChronicDuration.parse(string)
365
+ elsif string.scan(/\d/).any?
366
+ set_time = string.partition("at").last.strip!
367
+ set_time = Chronic.parse(set_time)
368
+ else
369
+ set_time = string.partition("at").last.strip!
370
+ set_time = parse_time(set_time)
371
+ end
372
+ end
373
+
374
+ def authorize_user
375
+ begin
376
+ if HTTParty.get("http://#{@ip}/api/#{@user}/config").include?("whitelist") == false
377
+ body = {:devicetype => "Hue_Switch", :username=>"1234567890"}
378
+ create_user = HTTParty.post("http://#{@ip}/api", :body => body.to_json)
379
+ puts "You need to press the link button on the bridge and run again" if create_user.first.include?("error")
380
+ end
381
+ rescue Errno::ECONNREFUSED
382
+ puts "Cannot Reach Bridge"
383
+ end
384
+ end
385
+
386
+ def populate_switch
387
+ @colors = {red: 65280, pink: 56100, purple: 52180, violet: 47188, blue: 46920, turquoise: 31146, green: 25500, yellow: 12750, orange: 8618}
388
+ @mired_colors = {candle: 500, relax: 467, reading: 346, neutral: 300, concentrate: 231, energize: 136}
389
+ @scenes = [] ; HTTParty.get("http://#{@ip}/api/#{@user}/scenes").keys.each { |k| @scenes.push(k) }
390
+ @groups = {} ; HTTParty.get("http://#{@ip}/api/#{@user}/groups").each { |k,v| @groups["#{v['name']}".downcase] = k } ; @groups["all"] = "0"
391
+ @lights = {} ; HTTParty.get("http://#{@ip}/api/#{@user}/lights").each { |k,v| @lights["#{v['name']}".downcase] = k }
392
+ end
393
+
394
+ def get_bridge_by_SSDP
395
+ discovered_devices = Hue.devices
396
+ bridge = discovered_devices.each do |device|
397
+ next unless device.get_response.include?("hue Personal")
398
+ end.first
399
+ end
400
+ end
401
+
402
+
403
+ # The Device and SSDP classes are basically lifted from Sam Soffes' great GitHub repo:
404
+ # https://github.com/soffes/discover.
405
+
406
+ class Device
407
+ attr_reader :ip
408
+ attr_reader :port
409
+ attr_reader :description_url
410
+ attr_reader :server
411
+ attr_reader :service_type
412
+ attr_reader :usn
413
+ attr_reader :url_base
414
+ attr_reader :name
415
+ attr_reader :manufacturer
416
+ attr_reader :manufacturer_url
417
+ attr_reader :model_name
418
+ attr_reader :model_number
419
+ attr_reader :model_description
420
+ attr_reader :model_url
421
+ attr_reader :serial_number
422
+ attr_reader :software_version
423
+ attr_reader :hardware_version
424
+
425
+ def initialize(info)
426
+ headers = {}
427
+ info[0].split("\r\n").each do |line|
428
+ matches = line.match(/^([\w\-]+):(?:\s)*(.*)$/)
429
+ next unless matches
430
+ headers[matches[1].upcase] = matches[2]
431
+ end
432
+
433
+ @description_url = headers['LOCATION']
434
+ @server = headers['SERVER']
435
+ @service_type = headers['ST']
436
+ @usn = headers['USN']
437
+
438
+ info = info[1]
439
+ @port = info[1]
440
+ @ip = info[2]
441
+ end
442
+
443
+ def get_response
444
+ Net::HTTP.get_response(URI.parse(description_url)).body
445
+ end
446
+ end
447
+
448
+ class SSDP
449
+ # SSDP multicast IPv4 address
450
+ MULTICAST_ADDR = '239.255.255.250'.freeze
451
+
452
+ # SSDP UDP port
453
+ MULTICAST_PORT = 1900.freeze
454
+
455
+ # Listen for all devices
456
+ DEFAULT_SERVICE_TYPE = 'ssdp:all'.freeze
457
+
458
+ # Timeout in 2 seconds
459
+ DEFAULT_TIMEOUT = 2.freeze
460
+
461
+ attr_reader :service_type
462
+ attr_reader :timeout
463
+ attr_reader :first
464
+
465
+ # @param service_type [String] the identifier of the device you're trying to find
466
+ # @param timeout [Fixnum] timeout in seconds
467
+ def initialize(options = {})
468
+ @service_type = options[:service_type]
469
+ @timeout = (options[:timeout] || DEFAULT_TIMEOUT)
470
+ @first = options[:first]
471
+ initialize_socket
472
+ end
473
+
474
+ # Look for devices on the network
475
+ def devices
476
+ @socket.send(search_message, 0, MULTICAST_ADDR, MULTICAST_PORT)
477
+ listen_for_responses(first)
478
+ end
479
+
480
+ private
481
+
482
+ def listen_for_responses(first = false)
483
+ @socket.send(search_message, 0, MULTICAST_ADDR, MULTICAST_PORT)
484
+
485
+ devices = []
486
+ Timeout::timeout(timeout) do
487
+ loop do
488
+ device = Device.new(@socket.recvfrom(2048))
489
+ next if service_type && service_type != device.service_type
490
+
491
+ if first
492
+ return device
493
+ else
494
+ devices << device
495
+ end
496
+ end
497
+ end
498
+ devices
499
+
500
+ rescue Timeout::Error => ex
501
+ devices
502
+ end
503
+
504
+ def initialize_socket
505
+ # Create a socket
506
+ @socket = UDPSocket.open
507
+
508
+ # We're going to use IP with the multicast TTL. Mystery third parameter is a mystery.
509
+ @socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, 2)
510
+ end
511
+
512
+ def search_message
513
+ [
514
+ 'M-SEARCH * HTTP/1.1',
515
+ "HOST: #{MULTICAST_ADDR}:reservedSSDPport",
516
+ 'MAN: ssdp:discover',
517
+ "MX: #{timeout}",
518
+ "ST: #{service_type || DEFAULT_SERVICE_TYPE}"
519
+ ].join("\n")
520
+ end
521
+ end
522
+ end
@@ -0,0 +1,7 @@
1
+ module Sinatra
2
+ module Hue
3
+ def self.intent_schema
4
+ File.read(File.expand_path('../../../skills_config/intent_schema.txt', __FILE__))
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Sinatra
2
+ module Hue
3
+ def self.sample_utterances
4
+ File.read(File.expand_path('../../../skills_config/sample_utterances.txt', __FILE__))
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ module Sinatra
2
+ module Hue
3
+ VERSION = "1.0.0"
4
+ end
5
+ end
data/lib/alexa_hue.rb ADDED
@@ -0,0 +1,144 @@
1
+ require 'json'
2
+ require 'alexa_objects'
3
+ require 'httparty'
4
+ require 'numbers_in_words'
5
+ require 'numbers_in_words/duck_punch'
6
+ require 'chronic'
7
+ require 'alexa_hue/version'
8
+ require 'sinatra/base'
9
+ require 'numbers_in_words/duck_punch'
10
+ require 'alexa_hue/hue_switch'
11
+ require 'chronic_duration'
12
+ require 'alexa_hue/fix_schedule_syntax'
13
+
14
+
15
+ LEVELS = {} ; [*1..10].each { |t| LEVELS[t.to_s ] = t.in_words }
16
+
17
+ module Sinatra
18
+ module Hue
19
+ def self.registered(app)
20
+ app.post '/alexa_hue' do
21
+ content_type :json
22
+
23
+ # halt 400, "Invalid Application ID" unless @application_id == "your application id here"
24
+
25
+ # Behavior for Launch Request
26
+ if @echo_request.launch_request?
27
+ response = AlexaObjects::Response.new
28
+ response.spoken_response = "I'm ready to control the lights"
29
+ response.end_session = false
30
+ response.without_card.to_json
31
+
32
+ # Behavior for ControlLights intent. Keys and values need to be adjusted a bit to work with HueSwitch #voice syntax.
33
+ elsif @echo_request.intent_name == "ControlLights"
34
+ if @echo_request.slots.brightness
35
+ LEVELS.keys.reverse_each { |level| @echo_request.slots.brightness.sub!(level, LEVELS[level]) } if @echo_request.slots.schedule.nil?
36
+ end
37
+
38
+ if @echo_request.slots.saturation
39
+ LEVELS.keys.reverse_each { |level| @echo_request.slots.saturation.sub!(level, LEVELS[level]) } if @echo_request.slots.schedule.nil?
40
+ end
41
+
42
+ puts @echo_request.slots
43
+ @echo_request.slots.to_h.each do |k,v|
44
+ @string ||= ""
45
+ next unless v
46
+ if k == :scene || k == :alert
47
+ @string << "#{v.to_s} #{k.to_s} "
48
+ elsif k == :lights || k == :modifier || k == :state
49
+ @string << "#{v.to_s} "
50
+ elsif k == :savescene
51
+ @string << "save scene as #{v.to_s} "
52
+ elsif k == :flash
53
+ @string << "start long alert "
54
+ else
55
+ @string << "#{k.to_s} #{v.to_s} "
56
+ end
57
+ end
58
+
59
+ fix_schedule_syntax(@string)
60
+ @string.sub!("color loop", "colorloop")
61
+ @string.strip!
62
+
63
+ begin
64
+ switch = Hue::Switch.new
65
+ rescue RuntimeError
66
+ response = AlexaObjects::Response.new
67
+ response.end_session = true
68
+ response.spoken_response = "Hello. Before using Hue lighting, you'll need to give me access to your Hue bridge." +
69
+ " Please press the link button on your bridge and launch the skill again within ten seconds."
70
+ halt response.without_card.to_json
71
+ end
72
+
73
+ if @echo_request.slots.lights.nil? && @echo_request.slots.scene.nil? && @echo_request.slots.savescene.nil?
74
+ r = AlexaObjects::Response.new
75
+ r.end_session = false
76
+ r.spoken_response = "Please specify which light or lights you'd like to adjust. I'm ready to control the lights."
77
+ halt r.without_card.to_json
78
+ end
79
+
80
+ if @echo_request.slots.lights
81
+ if @echo_request.slots.lights.scan(/light|lights/).empty?
82
+ r = AlexaObjects::Response.new
83
+ r.end_session = false
84
+ r.spoken_response = "Please specify which light or lights you'd like to adjust. I'm ready to control the lights."
85
+ halt r.without_card.to_json
86
+ end
87
+ end
88
+
89
+ puts @echo_request.slots.lights
90
+ if @echo_request.slots.lights
91
+ if @echo_request.slots.lights.include?('lights')
92
+ puts switch.list_groups.keys.join(', ').downcase
93
+ if !(switch.list_groups.keys.join(', ').downcase.include?("#{@echo_request.slots.lights.sub(' lights','')}"))
94
+ r = AlexaObjects::Response.new
95
+ r.end_session = true
96
+ r.spoken_response = "I couldn't find a group with the name #{@echo_request.slots.lights}"
97
+ halt r.without_card.to_json
98
+ end
99
+
100
+ elsif @echo_request.slots.lights.include?('light')
101
+ if !(switch.list_lights.keys.join(', ').downcase.include?("#{@echo_request.slots.lights.sub(' light','')}"))
102
+ r = AlexaObjects::Response.new
103
+ r.end_session = true
104
+ r.spoken_response = "I couldn't find a light with the name #{@echo_request.slots.lights}"
105
+ halt r.without_card.to_json
106
+ end
107
+ end
108
+ end
109
+
110
+
111
+ #if @string.include?('light ')
112
+ # if (@string.split(' ') & switch.list_lights.keys.join(', ').downcase.split(', ')).empty?
113
+ # r = AlexaObjects::Response.new
114
+ # r.end_session = true
115
+ # r.spoken_response = "I couldn't find a light with the name #{@echo_request.slots.lights}"
116
+ # halt r.without_card.to_json
117
+ # end
118
+ #end
119
+ switch.voice @string
120
+
121
+ response = AlexaObjects::Response.new
122
+ response.end_session = true
123
+ response.spoken_response = "okay"
124
+ response.card_content = "#{@string}...#{@data}"
125
+ response.without_card.to_json # change to .with_card.to_json for debugging info
126
+
127
+ # Behavior for End Session requests.
128
+ elsif @echo_request.intent_name == "EndSession"
129
+ response = AlexaObjects::Response.new
130
+ response.end_session = true
131
+ response.spoken_response = "exiting lighting"
132
+ response.without_card.to_json
133
+
134
+ elsif @echo_request.session_ended_request?
135
+ response = AlexaObjects::Response.new
136
+ response.end_session = true
137
+ response.without_card.to_json
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ register Hue
144
+ end
Binary file
@@ -0,0 +1,46 @@
1
+ LIGHTS
2
+ bedroom lights
3
+ fireplace lights
4
+ floor light
5
+ overhead light
6
+
7
+ ATTRIBUTE
8
+ Saturation
9
+ Brightness
10
+
11
+ COLOR
12
+ Red
13
+ Blue
14
+ Green
15
+ Yellow
16
+ Pink
17
+ Purple
18
+ Violet
19
+ Turquiose
20
+ Reading
21
+ Relax
22
+ Candle
23
+ Neutral
24
+ Concentrate
25
+
26
+ LEVEL
27
+ one
28
+ two
29
+ three
30
+ four
31
+ five
32
+ six
33
+ seven
34
+ eight
35
+ nine
36
+ ten
37
+
38
+ SCENE
39
+ morning
40
+ evening
41
+ dinner
42
+ afternoon
43
+
44
+ STATE
45
+ on
46
+ off
@@ -0,0 +1,65 @@
1
+ {
2
+ "intents": [
3
+ {
4
+ "intent": "ControlLights",
5
+ "slots": [
6
+ {
7
+ "name": "Lights",
8
+ "type": "LIGHTS"
9
+ },
10
+ {
11
+ "name": "Brightness",
12
+ "type": "LEVEL"
13
+ },
14
+ {
15
+ "name": "Saturation",
16
+ "type": "LEVEL"
17
+ },
18
+ {
19
+ "name": "Start",
20
+ "type": "AMAZON.LITERAL"
21
+ },
22
+ {
23
+ "name": "Stop",
24
+ "type": "AMAZON.LITERAL"
25
+ },
26
+ {
27
+ "name": "State",
28
+ "type": "STATE"
29
+ },
30
+ {
31
+ "name": "SaveScene",
32
+ "type": "AMAZON.LITERAL"
33
+ },
34
+ {
35
+ "name": "String",
36
+ "type": "AMAZON.LITERAL"
37
+ },
38
+ {
39
+ "name": "Color",
40
+ "type": "COLOR"
41
+ },
42
+ {
43
+ "name": "Schedule",
44
+ "type": "AMAZON.DURATION"
45
+ },
46
+ {
47
+ "name": "Time",
48
+ "type": "AMAZON.TIME"
49
+ },
50
+ {
51
+ "name": "Flash",
52
+ "type": "AMAZON.LITERAL"
53
+ },
54
+ {
55
+ "name": "Scene",
56
+ "type": "SCENE"
57
+ },
58
+ {
59
+ "name": "EndSession",
60
+ "type": "AMAZON.LITERAL"
61
+ }
62
+ ]
63
+ }
64
+ ]
65
+ }
@@ -0,0 +1,68 @@
1
+ ControlLights turn the {Lights} {State}
2
+ ControlLights turn {State} the {Lights}
3
+ ControlLights set {Scene} scene
4
+ ControlLights set scene {Scene}
5
+ ControlLights set the scene {Scene}
6
+
7
+ ControlLights turn the {Lights} {Color}
8
+ ControlLights turn the {Lights} to {Color}
9
+ ControlLights turn the {Lights} to the {Color}
10
+ ControlLights change the {Lights} to {Color}
11
+ ControlLights change the {Lights} to the {Color}
12
+ ControlLights set the {Lights} to {Color}
13
+ ControlLights set the {Lights} to the {Color}
14
+
15
+ ControlLights turn {State} the {Lights} in {Schedule}
16
+ ControlLights turn the {Lights} {State} in {Schedule}
17
+ ControlLights turn the {Lights} {Color} in {Schedule}
18
+ ControlLights turn the {Lights} to {Color} in {Schedule}
19
+ ControlLights turn the {Lights} to the {Color} in {Schedule}
20
+ ControlLights change the {Lights} to {Color} in {Schedule}
21
+ ControlLights change the {Lights} to the {Color} in {Schedule}
22
+ ControlLights set the {Lights} to {Color} in {Schedule}
23
+ ControlLights set the {Lights} to the {Color} in {Schedule}
24
+
25
+ ControlLights turn {State} the {Lights} at {Time}
26
+ ControlLights turn the {Lights} {State} at {Time}
27
+ ControlLights turn the {Lights} {Color} at {Time}
28
+ ControlLights turn the {Lights} to {Color} at {Time}
29
+ ControlLights turn the {Lights} to the {Color} at {Time}
30
+ ControlLights change the {Lights} to {Color} at {Time}
31
+ ControlLights change the {Lights} to the {Color} at {Time}
32
+ ControlLights set the {Lights} to {Color} at {Time}
33
+ ControlLights set the {Lights} to the {Color} at {Time}
34
+
35
+ ControlLights {Lights} brightness {Brightness}
36
+ ControlLights set the {Lights} to brightness {Brightness}
37
+ ControlLights raise the {Lights} to brightness {Brightness}
38
+ ControlLights dim the {Lights} to brightness {Brightness}
39
+ ControlLights {Lights} saturation {Saturation}
40
+ ControlLights set the {Lights} to saturation {Saturation}
41
+
42
+
43
+ ControlLights set the {Lights} to the color {Color} and saturation {Saturation}
44
+ ControlLights set the {Lights} to the color {Color} and brightness {Brightness}
45
+ ControlLights set the {Lights} to the color {Color} and saturation {Saturation} in {Schedule}
46
+ ControlLights set the {Lights} to the color {Color} and brightness {Brightness} in {Schedule}
47
+
48
+ ControlLights start {short alert|Start} on the {Lights}
49
+ ControlLights start {long alert|Start} on the {Lights}
50
+ ControlLights start {color loop|Start} on the {Lights}
51
+ ControlLights stop {alert|Stop}
52
+ ControlLights stop {color loop|Stop}
53
+
54
+ ControlLights {flash|Flash} {Lights}
55
+ ControlLights {flash|Flash} {Lights} in {Schedule}
56
+ ControlLights {flash|Flash} the {Lights}
57
+ ControlLights {flash|Flash} the {Lights} in {Schedule}
58
+
59
+ ControlLights save scene as {romantic|SaveScene}
60
+ ControlLights save scene as {summer dawn|SaveScene}
61
+ ControlLights save scene as {working at night|SaveScene}
62
+ ControlLights save scene as {purple|SaveScene}
63
+ ControlLights save scene as {midnight cave|SaveScene}
64
+
65
+ ControlLights {exit|EndSession}
66
+ ControlLights {quit|EndSession}
67
+ ControlLights {enough|EndSession}
68
+ ControlLights {stop|EndSession}
metadata ADDED
@@ -0,0 +1,245 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: alexa_hue
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Kyle Lucas
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-02-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: alexa_objects
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: httparty
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: numbers_in_words
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: activesupport
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: chronic
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: chronic_duration
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: sinatra
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: bundler
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '1.6'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '1.6'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rake
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rspec
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: webmock
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: codeclimate-test-reporter
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ description: ''
196
+ email:
197
+ - kglucas93@gmail.com
198
+ executables: []
199
+ extensions: []
200
+ extra_rdoc_files: []
201
+ files:
202
+ - ".DS_Store"
203
+ - ".gitignore"
204
+ - Gemfile
205
+ - Guardfile
206
+ - Rakefile
207
+ - alexa_hue.gemspec
208
+ - lib/alexa_hue.rb
209
+ - lib/alexa_hue/custom_slots.rb
210
+ - lib/alexa_hue/fix_schedule_syntax.rb
211
+ - lib/alexa_hue/hue_switch.rb
212
+ - lib/alexa_hue/intent_schema.rb
213
+ - lib/alexa_hue/sample_utterances.rb
214
+ - lib/alexa_hue/version.rb
215
+ - skills_config/.DS_Store
216
+ - skills_config/custom_slots.txt
217
+ - skills_config/intent_schema.txt
218
+ - skills_config/sample_utterances.txt
219
+ homepage: http://github.com/kylegrantlucas/alexa_hue
220
+ licenses:
221
+ - MIT
222
+ metadata: {}
223
+ post_install_message:
224
+ rdoc_options: []
225
+ require_paths:
226
+ - lib
227
+ - skills_config
228
+ required_ruby_version: !ruby/object:Gem::Requirement
229
+ requirements:
230
+ - - ">="
231
+ - !ruby/object:Gem::Version
232
+ version: 1.9.3
233
+ required_rubygems_version: !ruby/object:Gem::Requirement
234
+ requirements:
235
+ - - ">="
236
+ - !ruby/object:Gem::Version
237
+ version: '0'
238
+ requirements: []
239
+ rubyforge_project:
240
+ rubygems_version: 2.4.8
241
+ signing_key:
242
+ specification_version: 4
243
+ summary: A sinatra middleware for alexa hue actions.
244
+ test_files: []
245
+ has_rdoc: