alexa_hue 1.1.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 939ace937c9a84b49de17edb7195b74a5a6dc433
4
- data.tar.gz: de31f09e481c0b904f704ec7b33a3349aa0f0827
3
+ metadata.gz: 966f685e91dc2b39bca4fe3460b3e54dcf9ad350
4
+ data.tar.gz: 3695c7c36fe1ca51430e386661d54c0ee23e0051
5
5
  SHA512:
6
- metadata.gz: c375f8c8205baf13a4d7b7b055bc8778bb96a2826b18a0ba6f00d30603a9f0bb839f8078e36006ac4de429b40a78d7454bf3b1897c5e818f8dbc4ed014e5fd98
7
- data.tar.gz: 8033ac7150f3fb23608e593d3c26e9d79862fedb7b86e4aa0d059027c19910e38d78ac89bdf86ba61e59b99769da5540e9daacc0dfc9413a79c31d0dcba91a9f
6
+ metadata.gz: 7c57e8457a958dddf91067cc04adf62f0927180fe9fd82acc02bfa56b6b50ad8ec34243e7f6faf7f0c734fb9235b17d42576ca5f846af3b8643f8cb069d317fc
7
+ data.tar.gz: af8afc1312c35e9910965bea816dbc7f57178b7642c59243855c0e811f85ad276d8ac59959b5b6d31b84a2118ca4fa58e750b9ce5851031b30cf5feebac75b79
data/Gemfile CHANGED
@@ -9,6 +9,7 @@ gem 'numbers_in_words', '~> 0.2.0'
9
9
  gem 'sinatra'
10
10
  gem 'alexa_objects'
11
11
  gem 'activesupport'
12
+ gem 'takeout', '~> 1.0.6'
12
13
 
13
14
  group :development do
14
15
  gem "guard", "2.12.5", require: false
data/alexa_hue.gemspec CHANGED
@@ -26,7 +26,8 @@ Gem::Specification.new do |spec|
26
26
  spec.add_runtime_dependency 'activesupport'
27
27
  spec.add_runtime_dependency 'chronic'
28
28
  spec.add_runtime_dependency 'chronic_duration'
29
- spec.add_runtime_dependency 'sinatra'
29
+ spec.add_runtime_dependency 'takeout', '~> 1.0.6'
30
+ spec.add_runtime_dependency 'sinatra-contrib'
30
31
 
31
32
  spec.add_development_dependency "bundler", "~> 1.6"
32
33
  spec.add_development_dependency "rake"
@@ -0,0 +1,216 @@
1
+ require 'takeout'
2
+ require 'httparty'
3
+ require 'alexa_hue/hue/helpers'
4
+ require 'alexa_hue/hue/request_body'
5
+
6
+
7
+ module Hue
8
+ class Client
9
+ include Hue::Helpers
10
+ attr_accessor :client, :user, :bridge_ip, :scenes, :groups, :lights, :schedule_ids, :schedule_params, :command, :_group
11
+
12
+ def initialize(options={}, &block)
13
+ # JUST UPNP SUPPORT FOR NOW
14
+ @bridge_ip = HTTParty.get("https://www.meethue.com/api/nupnp").first["internalipaddress"] rescue nil
15
+ @user = "1234567890"
16
+ @groups, @lights, @scenes = {}, {}, []
17
+ prefix = "/api/#{@user}"
18
+ schemas = {
19
+ get: {
20
+ all_lights: "#{prefix}/groups/0",
21
+ group: "#{prefix}/groups/{{group}}",
22
+ root: "#{prefix}/"
23
+ },
24
+ put: {
25
+ scene: "#{prefix}/scenes/{{scene}}",
26
+ light: "#{prefix}/lights/{{lights}}/state",
27
+ group: "#{prefix}/groups/{{group}}/action",
28
+ all_lights: "#{prefix}/groups/0"
29
+ },
30
+ delete: {
31
+ schedule: "#{prefix}/schedules/{{schedule}}"
32
+ },
33
+ post: {
34
+ root: "#{prefix}/"
35
+ }
36
+ }
37
+
38
+ @client = Takeout::Client.new(uri: @bridge_ip, endpoint_prefix: prefix, schemas: schemas, debug: true, headers: { "Expect" => "100-continue" })
39
+
40
+ authorize_user
41
+ populate_client
42
+
43
+ @lights_array, @schedule_ids, @schedule_params, @command, @_group, @body = [], [], "", "0", Hue::RequestBody.new
44
+
45
+ # TODO: Do blocks right
46
+ instance_eval(&block) if block_given?
47
+ end
48
+
49
+ def confirm
50
+ @client.put_all_lights(alert: 'select')
51
+ end
52
+
53
+ def hue(numeric_value)
54
+ @body.reset
55
+ @body.hue = numeric_value
56
+ end
57
+
58
+ def mired(numeric_value)
59
+ @body.reset
60
+ @body.ct = numeric_value
61
+ end
62
+
63
+ def color(color_name)
64
+ @body.reset
65
+ @body.hue = @colors.keys.include?(color_name.to_sym) ?
66
+ @colors[color_name.to_sym] :
67
+ @mired_colors[color_name.to_sym]
68
+ end
69
+
70
+ def saturation(depth)
71
+ @body.clear_scene
72
+ @body.sat = depth
73
+ end
74
+
75
+ def brightness(depth)
76
+ @body.clear_scene
77
+ @body.bri = depth
78
+ end
79
+
80
+ def fade(in_seconds)
81
+ @body.transitiontime = in_seconds * 10
82
+ end
83
+
84
+ def light (*args)
85
+ @lights_array = []
86
+ @_group = ""
87
+ @body.clear_scene
88
+ args.each { |l| @lights_array.push @lights[l.to_s] if @lights.keys.include?(l.to_s) }
89
+ end
90
+
91
+ def lights(group_name)
92
+ @lights_array = []
93
+ @body.clear_scene
94
+ group = @groups[group_name.to_s]
95
+ @_group = group if !group.nil?
96
+ end
97
+
98
+ def scene(scene_name)
99
+ @body.reset
100
+ scene_details = @scenes[scene_name]
101
+ @lights_array = scene_details["lights"]
102
+ @_group = "0"
103
+ @body.scene = scene_details["id"]
104
+ end
105
+
106
+ def save_scene(scene_name)
107
+ fade(2) if @body.transitiontime == nil
108
+ light_group = @_group.empty? ? @client.get_all_lights.body["lights"] : @client.get_group(group: @_group).body["lights"]
109
+ params = {name: scene_name.gsub!(' ','-'), lights: light_group, transitiontime: @body.transitiontime}
110
+ response = @client.put_scene(scene: scene_name, options: params).body
111
+ confirm if response.first.keys[0] == "success"
112
+ end
113
+
114
+ def toggle_lights
115
+ @lights_array.each { |l| @client.put_lights({lights: l}.merge(@body.to_hash)) }
116
+ end
117
+
118
+ def toggle_group
119
+ @client.put_group({group: @_group}.merge(@body.to_hash(without_scene: true)))
120
+ end
121
+
122
+ def toggle_scene
123
+ if @body.on
124
+ @client.put_group({group: @_group}.merge(@body.to_hash(without_scene: true)))
125
+ else
126
+ @client.get_scenes[@body[:scene]]["lights"].each do |l|
127
+ @client.put_lights({lights: l}.merge(@body.to_hash))
128
+ end
129
+ end
130
+ end
131
+
132
+ def toggle_system
133
+ toggle_lights if @lights_array.any?
134
+ toggle_group if (!@_group.empty? && @body.scene.nil?)
135
+ toggle_scene if @body.scene
136
+ end
137
+
138
+ def on
139
+ @body.on = true
140
+ toggle_system
141
+ end
142
+
143
+ def off
144
+ @body.on = false
145
+ toggle_system
146
+ end
147
+
148
+ def schedule(string, on_or_off = :default)
149
+ @body.on = (on_or_off == :on)
150
+ set_time = set_time(string)
151
+ unless set_time < Time.now
152
+ set_time = set_time.to_s.split(' ')[0..1].join(' ').sub(' ',"T")
153
+ @schedule_params = {:name=>"Hue_Switch Alarm",
154
+ :description=>"",
155
+ :localtime=>"#{set_time}",
156
+ :status=>"enabled",
157
+ :autodelete=>true
158
+ }
159
+ if @lights_array.any?
160
+ lights_array.each {|l| @schedule_params[:command] = {:address=>"/api/#{@user}/lights/#{l}/state", :method=>"PUT", :body=>@body} }
161
+ else
162
+ @schedule_params[:command] = {:address=>"/api/#{@user}/groups/#{@_group}/action", :method=>"PUT", :body=>@body}
163
+ end
164
+ @schedule_ids.push(@client.post_schedules(@schedule_params).body)
165
+ confirm if @schedule_ids.flatten.last.include?("success")
166
+ end
167
+ end
168
+
169
+ def delete_schedules!
170
+ @schedule_ids.flatten!
171
+ @schedule_ids.each { |k| @client.delete_schedule(schedule: k.dig("success","id")) }
172
+ @schedule_ids = []
173
+ end
174
+
175
+ def colorloop(start_or_stop)
176
+ @body.effect = (start_or_stop == :start) ? "colorloop" : "none"
177
+ end
178
+
179
+ def alert(value)
180
+ if value == :short
181
+ @body.alert = "select"
182
+ elsif value == :long
183
+ @body.alert = "lselect"
184
+ elsif value == :stop
185
+ @body.alert = "none"
186
+ end
187
+ end
188
+
189
+ def reset
190
+ @command, @_group, @body, @schedule_params = "", "0", Hue::RequestBody.new, nil
191
+ end
192
+
193
+ private
194
+
195
+ def authorize_user
196
+ begin
197
+ if @client.get_config.body.include?("whitelist") == false
198
+ body = {:devicetype => "Hue_Switch", :username=>"1234567890"}
199
+ create_user = @client.post_root(body).body
200
+ puts "You need to press the link button on the bridge and run again" if create_user.first.include?("error")
201
+ end
202
+ rescue Errno::ECONNREFUSED
203
+ puts "Cannot Reach Bridge"
204
+ end
205
+ end
206
+
207
+ def populate_client
208
+ @colors = {red: 65280, pink: 56100, purple: 52180, violet: 47188, blue: 46920, turquoise: 31146, green: 25500, yellow: 12750, orange: 8618}
209
+ @mired_colors = {candle: 500, relax: 467, reading: 346, neutral: 300, concentrate: 231, energize: 136}
210
+ @scenes = {} ; @client.get_scenes.body.each { |s| @scenes.merge!({"#{s[1]["name"].split(' ').first.downcase}" => {"id" => s[0]}.merge(s[1])}) if s[1]["owner"] != "none"}
211
+ @groups = {} ; @client.get_groups.body.each { |k,v| @groups["#{v['name']}".downcase] = k } ; @groups["all"] = "0"
212
+ @lights = {} ; @client.get_lights.body.each { |k,v| @lights["#{v['name']}".downcase] = k }
213
+ end
214
+ end
215
+ end
216
+
@@ -0,0 +1,62 @@
1
+ require 'chronic'
2
+ require 'chronic_duration'
3
+ require 'numbers_in_words'
4
+ require 'numbers_in_words/duck_punch'
5
+
6
+ module Hue
7
+ module Helpers
8
+ def numbers_to_times(numbers)
9
+ numbers.map!(&:in_numbers)
10
+ numbers.map!(&:to_s)
11
+ numbers.push("0") if numbers[1] == nil
12
+ numbers = numbers.shift + ':' + (numbers[0].to_i + numbers[1].to_i).to_s
13
+ numbers.gsub!(':', ':0') if numbers.split(":")[1].length < 2
14
+ numbers
15
+ end
16
+
17
+ def parse_time(string)
18
+ string.sub!(" noon", " twelve in the afternoon")
19
+ string.sub!("midnight", "twelve in the morning")
20
+ time_modifier = string.downcase.scan(/(evening)|(night|tonight)|(afternoon)|(pm)|(a.m.)|(am)|(p.m.)|(morning)|(today)/).flatten.compact.first
21
+ guess = Time.now.strftime('%H').to_i >= 12 ? "p.m." : "a.m."
22
+ time_modifier = time_modifier.nil? ? guess : time_modifier
23
+ day_modifier = string.scan(/(tomorrow)|(next )?(monday|tuesday|wednesday|thursday|friday|saturday|sunday)/).flatten.compact.join(' ')
24
+ numbers_in_words = string.scan(Regexp.union((1..59).map(&:in_words)))
25
+ set_time = numbers_to_times(numbers_in_words)
26
+ set_time = Chronic.parse(day_modifier + ' ' + set_time + ' ' + time_modifier)
27
+ end
28
+
29
+ def set_time(string)
30
+ if string.scan(/ seconds?| minutes?| hours?| days?| weeks?/).any?
31
+ set_time = string.partition("in").last.strip!
32
+ set_time = Time.now + ChronicDuration.parse(string)
33
+ elsif string.scan(/\d/).any?
34
+ set_time = string.partition("at").last.strip!
35
+ set_time = Chronic.parse(set_time)
36
+ else
37
+ set_time = string.partition("at").last.strip!
38
+ set_time = parse_time(set_time)
39
+ end
40
+ end
41
+
42
+ def fix_schedule_syntax(string)
43
+ sub_time = string.match(/time \d{2}:\d{2}/)
44
+ sub_duration = string.match(/schedule PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/)
45
+
46
+ if sub_time
47
+ sub_time = sub_time.to_s
48
+ string.slice!(sub_time).strip
49
+ string << " #{sub_time}"
50
+ string.sub!("time", "schedule at")
51
+ end
52
+
53
+ if sub_duration
54
+ sub_duration = sub_duration.to_s
55
+ string.slice!(sub_duration).strip
56
+ sub_duration = ChronicDuration.parse(sub_duration.split(' ').last)
57
+ string << " schedule in #{sub_duration} seconds"
58
+ end
59
+ string.strip if string
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,66 @@
1
+ require 'alexa_hue/hue/client'
2
+
3
+ module Hue
4
+ class JsClient < Client
5
+ attr_accessor :client, :user, :bridge_ip, :schedule_ids, :schedule_params, :command, :_group
6
+
7
+ def initialize(options={})
8
+ @client = Takeout::Client.new(uri: options[:uri], port: options[:port])
9
+ @lights_array, @schedule_ids, @schedule_params, @command, @_group, @body = [], [], "", "0", Hue::RequestBody.new
10
+
11
+ populate_client
12
+ end
13
+
14
+ def confirm
15
+ @client.apply_alert(:alert => 'select')
16
+ end
17
+
18
+ def save_scene(scene_name)
19
+ fade(2) if @body.transitiontime == nil
20
+ if @_group.empty?
21
+ light_group = @client.get_all_lights.body["lights"]
22
+ else
23
+ light_group = @client.get_group(group: @_group).body["lights"]
24
+ end
25
+ params = {name: scene_name.gsub!(' ','-'), lights: light_group, transitiontime: @body.transitiontime}
26
+ response = @client.put_scene(scene: scene_name, options: params).body
27
+ confirm if response.first.keys[0] == "success"
28
+ end
29
+
30
+ def delete_schedules!
31
+ @schedule_ids.flatten!
32
+ @schedule_ids.each { |k| @client.delete_schedule(schedule: k.dig("success","id")) }
33
+ @schedule_ids = []
34
+ end
35
+
36
+ def schedule(string, on_or_off = :default)
37
+ @body.on = (on_or_off == :on)
38
+ set_time = set_time(string)
39
+ unless set_time < Time.now
40
+ set_time = set_time.to_s.split(' ')[0..1].join(' ').sub(' ',"T")
41
+ @schedule_params = {:name=>"Hue_Switch Alarm",
42
+ :description=>"",
43
+ :localtime=>"#{set_time}",
44
+ :status=>"enabled",
45
+ :autodelete=>true
46
+ }
47
+ if @lights_array.any?
48
+ lights_array.each {|l| @schedule_params[:command] = {:address=>"/api/#{@user}/lights/#{l}/state", :method=>"PUT", :body=>@body} }
49
+ else
50
+ @schedule_params[:command] = {:address=>"/api/#{@user}/groups/#{@_group}/action", :method=>"PUT", :body=>@body}
51
+ end
52
+ @schedule_ids.push(@client.post_schedules(options: @schedule_params).body)
53
+ confirm if @schedule_ids.flatten.last.include?("success")
54
+ end
55
+ end
56
+
57
+ def on
58
+ if @body.scene
59
+ @client.get_activate_scene(scene: @body.scene)
60
+ end
61
+ end
62
+
63
+ def off
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,29 @@
1
+ require 'json'
2
+
3
+ module Hue
4
+ class RequestBody
5
+ attr_accessor :hue, :ct, :bri, :scene, :sat, :transitiontime, :on, :effect, :alert
6
+
7
+ def initialize(options={})
8
+ options.each {|k,v| self.send("#{k}=".to_sym, v)}
9
+ end
10
+
11
+ def reset
12
+ @hue, @ct, @scene = nil, nil, nil
13
+ end
14
+
15
+ def clear_scene
16
+ @scene = nil
17
+ end
18
+
19
+ def to_json(without_scene:false)
20
+ return self.to_hash(without_scene: without_scene).to_json
21
+ end
22
+
23
+ def to_hash(without_scene:false)
24
+ hash = {hue: @hue, ct: @ct, bri: @bri, sat: @sat, transitiontime: @transitiontime, on: @on, effect: @effect, alert: alert}
25
+ hash.merge!(scene: @scene) if without_scene
26
+ return hash
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,80 @@
1
+ require 'alexa_hue/hue/client'
2
+ require 'alexa_hue/hue/js_client'
3
+ require 'alexa_hue/hue/helpers'
4
+
5
+ module Hue
6
+ class VoiceParser
7
+ include Hue::Helpers
8
+ attr_accessor :client
9
+
10
+ def initialize(options={})
11
+ @client = options[:js] ? Hue::JsClient.new(options[:js]) : Hue::Client.new
12
+ end
13
+
14
+ def voice(string)
15
+ @client.reset
16
+ @client.command << string
17
+
18
+ parse_voice(string)
19
+
20
+ if @client.command.include?("schedule")
21
+ state = string.match(/off|on/)[0].to_sym rescue nil
22
+ @client.schedule(*[string, state])
23
+ else
24
+ string.include?(' off') ? @client.off : @client.on
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def parse_leading(methods)
31
+ methods.each do |l|
32
+ capture = (@client.command.match (/\b#{l}\s\w+/)).to_s.split(' ')
33
+ method = capture[0]
34
+ value = capture[1]
35
+ value = value.in_numbers if value.scan(Regexp.union( (1..10).map {|k| k.in_words} ) ).any?
36
+ value = ((value.to_f/10.to_f)*255).to_i if (value.class == Fixnum) && (l != "fade")
37
+ @client.send( method, value )
38
+ end
39
+ end
40
+
41
+ def parse_trailing(method)
42
+ all_keys = Regexp.union((@client.groups.keys + @client.lights.keys).flatten)
43
+ value = @client.command.match(all_keys).to_s
44
+ @client.send(method.first, value)
45
+ end
46
+
47
+ def parse_dynamic(methods)
48
+ methods.each do |d|
49
+ capture = (@client.command.match (/\w+ #{d}\b/)).to_s.split(' ')
50
+ method = capture[1]
51
+ value = capture[0].to_sym
52
+ @client.send(method, value)
53
+ end
54
+ end
55
+
56
+ def parse_scene(scene_name)
57
+ scene_name.gsub!(' ','-') if scene_name.size > 1
58
+ @client.scene(scene_name)
59
+ end
60
+
61
+ def parse_save_scene
62
+ save_scene = @client.command.partition(/save (scene|seen) as /).last
63
+ @client.save_scene(save_scene)
64
+ end
65
+
66
+ def parse_voice(string)
67
+ string.gsub!('schedule ','')
68
+ trailing = string.split(' ') & %w[lights light]
69
+ leading = string.split(' ') & %w[hue brightness saturation fade color]
70
+ dynamic = string.split(' ') & %w[colorloop alert]
71
+ scene_name = string.partition(" scene").first
72
+
73
+ parse_scene(scene_name) if string.include?(" scene") && !string.include?("save")
74
+ parse_leading(leading) if leading.any?
75
+ parse_trailing(trailing) if trailing.any?
76
+ parse_dynamic(dynamic) if dynamic.any?
77
+ parse_save_scene if @client.command.scan(/save (scene|seen) as/).length > 0
78
+ end
79
+ end
80
+ end
@@ -1,4 +1,4 @@
1
1
  module Hue
2
- VERSION = "1.1.0"
2
+ VERSION = "1.1.1"
3
3
  end
4
4
 
data/lib/alexa_hue.rb CHANGED
@@ -5,29 +5,27 @@ require 'numbers_in_words'
5
5
  require 'numbers_in_words/duck_punch'
6
6
  require 'chronic'
7
7
  require 'alexa_hue/version'
8
- require 'sinatra/base'
8
+ require 'sinatra/extension'
9
9
  require 'numbers_in_words/duck_punch'
10
- require 'alexa_hue/hue_switch'
10
+ require 'alexa_hue/hue/voice_parser'
11
+ require 'alexa_hue/hue/helpers'
11
12
  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
13
 
18
14
  module Hue
15
+ include Hue::Helpers
19
16
  extend Sinatra::Extension
20
17
 
21
18
  helpers do
22
- def control_lights
23
- if @echo_request.slots.brightness
24
- LEVELS.keys.reverse_each { |level| @echo_request.slots.brightness.sub!(level, LEVELS[level]) } if @echo_request.slots.schedule.nil?
25
- end
19
+ LEVELS = [*1..10].map{|k,v|[k.to_s,t,in_words]}.to_h
26
20
 
27
- if @echo_request.slots.saturation
28
- LEVELS.keys.reverse_each { |level| @echo_request.slots.saturation.sub!(level, LEVELS[level]) } if @echo_request.slots.schedule.nil?
21
+ def control_lights
22
+ [:brightness, :satruation].each do |attribute|
23
+ if @echo_request.slots.send(attribute)
24
+ LEVELS.keys.reverse_each { |level| @echo_request.slots.send(attribute).sub!(level, LEVELS[level]) } if @echo_request.slots.schedule.nil?
25
+ end
29
26
  end
30
27
 
28
+ # TODO: ...something about this feel ugly and unexpressive
31
29
  @echo_request.slots.to_h.each do |k,v|
32
30
  @string ||= ""
33
31
  next unless v
@@ -43,51 +41,42 @@ module Hue
43
41
  @string << "#{k.to_s} #{v.to_s} "
44
42
  end
45
43
  end
46
-
47
- fix_schedule_syntax(@string)
48
- @string.sub!("color loop", "colorloop")
49
- @string.strip!
50
44
 
51
- begin
52
- switch = Hue::Switch.new
53
- rescue RuntimeError
54
- halt AlexaObjects::Response.new(spoken_response: "Hello. Before using Hue lighting, you'll need to give me access to your Hue bridge. Please press the link button on your bridge and launch the skill again within ten seconds.").to_json
55
- end
45
+ # TODO: create context for these statements
46
+ fix_schedule_syntax(@string)
47
+ @string.sub!("color loop", "colorloop").strip!
56
48
 
57
- if @echo_request.slots.lights.nil? && @echo_request.slots.scene.nil? && @echo_request.slots.savescene.nil?
58
- halt AlexaObjects::Response.new(end_session: false, spoken_response: "Please specify which light or lights you'd like to adjust. I'm ready to control the lights.").to_json
59
- end
60
-
61
- if @echo_request.slots.lights
62
- if @echo_request.slots.lights.scan(/light|lights/).empty?
49
+ if @echo_request.slots.scene.nil? && @echo_request.slots.savescene.nil?
50
+ if @echo_request.slots.lights.nil? || @echo_request.slots.lights&.scan(/light|lights/).empty?
63
51
  halt AlexaObjects::Response.new(end_session: false, spoken_response: "Please specify which light or lights you'd like to adjust. I'm ready to control the lights.").to_json
64
52
  end
65
53
  end
66
54
 
55
+ # TODO: 58-68 seem to both express the same thought from alexa, could be simplified?
67
56
  if @echo_request.slots.lights
68
57
  if @echo_request.slots.lights.include?('lights')
69
- if !(switch.list_groups.keys.join(', ').downcase.include?("#{@echo_request.slots.lights.sub(' lights','')}"))
58
+ if !(hue_client.list_groups.keys.join(', ').downcase.include?("#{@echo_request.slots.lights.sub(' lights','')}"))
70
59
  halt AlexaObjects::Response.new(spoken_response: "I couldn't find a group with the name #{@echo_request.slots.lights}").to_json
71
60
  end
72
61
  elsif @echo_request.slots.lights.include?('light')
73
- if !(switch.list_lights.keys.join(', ').downcase.include?("#{@echo_request.slots.lights.sub(' light','')}"))
62
+ if !(hue_client.list_lights.keys.join(', ').downcase.include?("#{@echo_request.slots.lights.sub(' light','')}"))
74
63
  halt AlexaObjects::Response.new(spoken_response: "I couldn't find a light with the name #{@echo_request.slots.lights}").to_json
75
64
  end
76
65
  end
77
66
  end
78
-
79
67
 
80
- #if @string.include?('light ')
81
- # if (@string.split(' ') & switch.list_lights.keys.join(', ').downcase.split(', ')).empty?
82
- # r = AlexaObjects::Response.new
83
- # r.end_session = true
84
- # r.spoken_response = "I couldn't find a light with the name #{@echo_request.slots.lights}"
85
- # halt r.without_card.to_json
86
- # end
87
- #end
88
- switch.voice @string
68
+ #TODO : majorly create context for this
69
+ hue_client.voice @string
89
70
 
90
71
  return AlexaObjects::Response.new(spoken_response: "okay").to_json
91
72
  end
73
+
74
+ def hue_client
75
+ begin
76
+ @client = Hue::Switch.new
77
+ rescue RuntimeError
78
+ halt AlexaObjects::Response.new(spoken_response: "Hello. Before using Hue lighting, you'll need to give me access to your Hue bridge. Please press the link button on your bridge and launch the skill again within ten seconds.").to_json
79
+ end
80
+ end
92
81
  end
93
82
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: alexa_hue
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kyle Lucas
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-02-17 00:00:00.000000000 Z
11
+ date: 2016-03-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -109,7 +109,21 @@ dependencies:
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0'
111
111
  - !ruby/object:Gem::Dependency
112
- name: sinatra
112
+ name: takeout
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 1.0.6
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 1.0.6
125
+ - !ruby/object:Gem::Dependency
126
+ name: sinatra-contrib
113
127
  requirement: !ruby/object:Gem::Requirement
114
128
  requirements:
115
129
  - - ">="
@@ -199,7 +213,6 @@ executables: []
199
213
  extensions: []
200
214
  extra_rdoc_files: []
201
215
  files:
202
- - ".DS_Store"
203
216
  - ".gitignore"
204
217
  - Gemfile
205
218
  - Guardfile
@@ -207,8 +220,11 @@ files:
207
220
  - alexa_hue.gemspec
208
221
  - lib/alexa_hue.rb
209
222
  - lib/alexa_hue/custom_slots.rb
210
- - lib/alexa_hue/fix_schedule_syntax.rb
211
- - lib/alexa_hue/hue_switch.rb
223
+ - lib/alexa_hue/hue/client.rb
224
+ - lib/alexa_hue/hue/helpers.rb
225
+ - lib/alexa_hue/hue/js_client.rb
226
+ - lib/alexa_hue/hue/request_body.rb
227
+ - lib/alexa_hue/hue/voice_parser.rb
212
228
  - lib/alexa_hue/intent_schema.rb
213
229
  - lib/alexa_hue/sample_utterances.rb
214
230
  - lib/alexa_hue/version.rb
@@ -236,7 +252,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
236
252
  version: '0'
237
253
  requirements: []
238
254
  rubyforge_project:
239
- rubygems_version: 2.4.8
255
+ rubygems_version: 2.5.1
240
256
  signing_key:
241
257
  specification_version: 4
242
258
  summary: A sinatra middleware for alexa hue actions.
data/.DS_Store DELETED
Binary file
@@ -1,19 +0,0 @@
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
@@ -1,498 +0,0 @@
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
- @user = "1234567890"
29
-
30
- begin
31
- @ip = HTTParty.get("https://www.meethue.com/api/nupnp").first["internalipaddress"] rescue nil
32
- @ip ||= get_bridge_by_SSDP.ip
33
- rescue
34
- puts "Cannot connect to bridge."
35
- end
36
-
37
- authorize_user
38
- populate_switch
39
-
40
- self.lights_array = []
41
- self.schedule_ids = []
42
- self.schedule_params = nil
43
- self.command = ""
44
- self._group = "0"
45
- self.body = {}
46
- instance_eval(&block) if block_given?
47
- end
48
-
49
- def list_lights
50
- HTTParty.get("http://#{@ip}/api/#{@user}/lights").map{|k,v|[k,"#{v['name']}".downcase]}.to_h
51
- end
52
-
53
- def list_groups
54
- HTTParty.get("http://#{@ip}/api/#{@user}/groups").map{|k,v|[k,"#{v['name']}".downcase]}.to_h.merge("all" =>
55
- "0")
56
- end
57
-
58
- def list_scenes
59
- scene_list = {}
60
- HTTParty.get("http://#{@ip}/api/#{@user}/scenes").each do |scene|
61
- if scene[1]["owner"] != "none"
62
- inner = {"id" => scene[0]}.merge(scene[1])
63
- scene_list.merge!({"#{scene[1]["name"].split(' ').first.downcase}" => inner})
64
- end
65
- end
66
- scene_list
67
- end
68
-
69
- def hue (numeric_value)
70
- clear_attributes
71
- self.body[:hue] = numeric_value
72
- end
73
-
74
- def mired (numeric_value)
75
- clear_attributes
76
- self.body[:ct] = numeric_value
77
- end
78
-
79
- def color(color_name)
80
- clear_attributes
81
- if @colors.keys.include?(color_name.to_sym)
82
- self.body[:hue] = @colors[color_name.to_sym]
83
- else
84
- self.body[:ct] = @mired_colors[color_name.to_sym]
85
- end
86
- end
87
-
88
- def saturation(depth)
89
- self.body.delete(:scene)
90
- self.body[:sat] = depth
91
- end
92
-
93
- def brightness(depth)
94
- self.body.delete(:scene)
95
- self.body[:bri] = depth
96
- end
97
-
98
- def clear_attributes
99
- self.body.delete_if{|k|[:scene,:ct,:hue].include?(k)}
100
- end
101
-
102
- def fade(in_seconds)
103
- self.body[:transitiontime] = in_seconds * 10
104
- end
105
-
106
- def light (*args)
107
- self.lights_array = []
108
- self._group = ""
109
- self.body.delete(:scene)
110
- args.each { |l| self.lights_array.push @lights[l.to_s] if @lights.keys.include?(l.to_s) }
111
- end
112
-
113
- def lights(group_name)
114
- self.lights_array = []
115
- self.body.delete(:scene)
116
- group = @groups[group_name.to_s]
117
- self._group = group if !group.nil?
118
- end
119
-
120
- def scene(scene_name)
121
- clear_attributes
122
- scene_details = self.list_scenes[scene_name]
123
- self.lights_array = scene_details["lights"]
124
- self._group = "0"
125
- self.body[:scene] = scene_details["id"]
126
- end
127
-
128
- def confirm
129
- HTTParty.put("http://#{@ip}/api/#{@user}/groups/0/action" , :body => {:alert => 'select'}.to_json)
130
- end
131
-
132
- def save_scene(scene_name)
133
- self.fade 2 if self.body[:transitiontime] == nil
134
- if self._group.empty?
135
- light_group = HTTParty.get("http://#{@ip}/api/#{@user}/groups/0")["lights"]
136
- else
137
- light_group = HTTParty.get("http://#{@ip}/api/#{@user}/groups/#{self._group}")["lights"]
138
- end
139
- params = {name: scene_name.gsub!(' ','-'), lights: light_group, transitiontime: self.body[:transitiontime]}
140
- response = HTTParty.put("http://#{@ip}/api/#{@user}/scenes/#{scene_name}", :body => params.to_json)
141
- confirm if response.first.keys[0] == "success"
142
- end
143
-
144
- def lights_on_off
145
- self.lights_array.each { |l| HTTParty.put("http://#{@ip}/api/#{@user}/lights/#{l}/state", :body => (self.body).to_json) }
146
- end
147
-
148
- def group_on_off
149
- HTTParty.put("http://#{@ip}/api/#{@user}/groups/#{self._group}/action", :body => (self.body.reject { |s| s == :scene }).to_json)
150
- end
151
-
152
- def scene_on_off
153
- if self.body[:on] == true
154
- HTTParty.put("http://#{@ip}/api/#{@user}/groups/#{self._group}/action", :body => (self.body.select { |s| s == :scene }).to_json)
155
- elsif self.body[:on] == false
156
- # turn off individual lights in the scene
157
- (HTTParty.get("http://#{@ip}/api/#{@user}/scenes"))[self.body[:scene]]["lights"].each do |l|
158
- HTTParty.put("http://#{@ip}/api/#{@user}/lights/#{l}/state", :body => (self.body).to_json)
159
- end
160
- end
161
- end
162
-
163
- def on
164
- self.body[:on]=true
165
- lights_on_off if self.lights_array.any?
166
- group_on_off if (!self._group.empty? && self.body[:scene].nil?)
167
- scene_on_off if !self.body[:scene].nil?
168
- end
169
-
170
- def off
171
- self.body[:on]=false
172
- lights_on_off if self.lights_array.any?
173
- group_on_off if (!self._group.empty? && self.body[:scene].nil?)
174
- scene_on_off if !self.body[:scene].nil?
175
- end
176
-
177
- # Parses times in words (e.g., "eight forty five") to standard HH:MM format
178
-
179
- def schedule (string, on_or_off = :default)
180
- self.body[:on] = true if on_or_off == :on
181
- self.body[:on] = false if on_or_off == :off
182
- set_time = set_time(string)
183
- if set_time < Time.now
184
- p "You've scheduled this in the past"
185
- else
186
- set_time = set_time.to_s.split(' ')[0..1].join(' ').sub(' ',"T")
187
- self.schedule_params = {:name=>"Hue_Switch Alarm",
188
- :description=>"",
189
- :localtime=>"#{set_time}",
190
- :status=>"enabled",
191
- :autodelete=>true
192
- }
193
- if self.lights_array.any?
194
- lights_array.each do |l|
195
- self.schedule_params[:command] = {:address=>"/api/#{@user}/lights/#{l}/state", :method=>"PUT", :body=>self.body}
196
- end
197
- else
198
- self.schedule_params[:command] = {:address=>"/api/#{@user}/groups/#{self._group}/action", :method=>"PUT", :body=>self.body}
199
- end
200
- self.schedule_ids.push(HTTParty.post("http://#{@ip}/api/#{@user}/schedules", :body => (self.schedule_params).to_json))
201
- confirm if self.schedule_ids.flatten.last.include?("success")
202
- end
203
- end
204
-
205
- def delete_schedules!
206
- self.schedule_ids.flatten!
207
- self.schedule_ids.each { |k|
208
- id = k["success"]["id"] if k.include?("success")
209
- HTTParty.delete("http://#{@ip}/api/#{@user}/schedules/#{id}")
210
- }
211
- self.schedule_ids = []
212
- end
213
-
214
- def colorloop(start_or_stop)
215
- if start_or_stop == :start
216
- self.body[:effect] = "colorloop"
217
- elsif start_or_stop == :stop
218
- self.body[:effect] = "none"
219
- end
220
- end
221
-
222
- def alert(value)
223
- if value == :short
224
- self.body[:alert] = "select"
225
- elsif value == :long
226
- self.body[:alert] = "lselect"
227
- elsif value == :stop
228
- self.body[:alert] = "none"
229
- end
230
- end
231
-
232
- def reset
233
- self.command = ""
234
- self._group = "0"
235
- self.body = {}
236
- self.schedule_params = nil
237
- end
238
-
239
- #The following two methods are required to use Switch with Zach Feldman's Alexa-home*
240
- def wake_words
241
- ["light", "lights", "scene", "seen"]
242
- end
243
-
244
- def process_command (command)
245
- command.sub!("color loop", "colorloop")
246
- command.sub!("too", "two")
247
- command.sub!("for", "four")
248
- command.sub!(/a half$/, 'thirty seconds')
249
- self.voice command
250
- end
251
-
252
- #The rest of the methods allow access to most of the Switch class functionality by supplying a single string
253
-
254
- def voice(string)
255
- self.reset
256
- self.command << string
257
-
258
- parse_voice(string)
259
-
260
- if self.command.include?("schedule")
261
- state = string.match(/off|on/)[0].to_sym rescue nil
262
- self.send("schedule", *[string, state])
263
- else
264
- string.include?(' off') ? self.send("off") : self.send("on")
265
- end
266
- end
267
-
268
- private
269
-
270
- def parse_leading(methods)
271
- methods.each do |l|
272
- capture = (self.command.match (/\b#{l}\s\w+/)).to_s.split(' ')
273
- method = capture[0]
274
- value = capture[1]
275
- value = value.in_numbers if value.scan(Regexp.union( (1..10).map {|k| k.in_words} ) ).any?
276
- value = ((value.to_f/10.to_f)*255).to_i if (value.class == Fixnum) && (l != "fade")
277
- self.send( method, value )
278
- end
279
- end
280
-
281
- def parse_trailing(method)
282
- all_keys = Regexp.union((@groups.keys + @lights.keys).flatten)
283
- value = self.command.match(all_keys).to_s
284
- self.send( method.first, value )
285
- end
286
-
287
- def parse_dynamic(methods)
288
- methods.each do |d|
289
- capture = (self.command.match (/\w+ #{d}\b/)).to_s.split(' ')
290
- method = capture[1]
291
- value = capture[0].to_sym
292
- self.send( method, value )
293
- end
294
- end
295
-
296
- def parse_scene(scene_name)
297
- scene_name.gsub!(' ','-') if scene_name.size > 1
298
- self.send("scene", scene_name)
299
- end
300
-
301
- def parse_save_scene
302
- save_scene = self.command.partition(/save (scene|seen) as /).last
303
- self.send( "save_scene", save_scene)
304
- end
305
-
306
- def parse_voice(string)
307
- string.gsub!('schedule ','')
308
- trailing = string.split(' ') & %w[lights light]
309
- leading = string.split(' ') & %w[hue brightness saturation fade color]
310
- dynamic = string.split(' ') & %w[colorloop alert]
311
- scene_name = string.partition(" scene").first
312
-
313
- parse_scene(scene_name) if string.include?(" scene") && !string.include?("save")
314
- parse_leading(leading) if leading.any?
315
- parse_trailing(trailing) if trailing.any?
316
- parse_dynamic(dynamic) if dynamic.any?
317
- parse_save_scene if self.command.scan(/save (scene|seen) as/).length > 0
318
- end
319
-
320
- def numbers_to_times(numbers)
321
- numbers.map!(&:in_numbers)
322
- numbers.map!(&:to_s)
323
- numbers.push("0") if numbers[1] == nil
324
- numbers = numbers.shift + ':' + (numbers[0].to_i + numbers[1].to_i).to_s
325
- numbers.gsub!(':', ':0') if numbers.split(":")[1].length < 2
326
- numbers
327
- end
328
-
329
- def parse_time(string)
330
- string.sub!(" noon", " twelve in the afternoon")
331
- string.sub!("midnight", "twelve in the morning")
332
- time_modifier = string.downcase.scan(/(evening)|(night|tonight)|(afternoon)|(pm)|(a.m.)|(am)|(p.m.)|(morning)|(today)/).flatten.compact.first
333
- guess = Time.now.strftime('%H').to_i >= 12 ? "p.m." : "a.m."
334
- time_modifier = time_modifier.nil? ? guess : time_modifier
335
- day_modifier = string.scan(/(tomorrow)|(next )?(monday|tuesday|wednesday|thursday|friday|saturday|sunday)/).flatten.compact.join(' ')
336
- numbers_in_words = string.scan(Regexp.union((1..59).map(&:in_words)))
337
- set_time = numbers_to_times(numbers_in_words)
338
- set_time = Chronic.parse(day_modifier + ' ' + set_time + ' ' + time_modifier)
339
- end
340
-
341
- def set_time(string)
342
- if string.scan(/ seconds?| minutes?| hours?| days?| weeks?/).any?
343
- set_time = string.partition("in").last.strip!
344
- set_time = Time.now + ChronicDuration.parse(string)
345
- elsif string.scan(/\d/).any?
346
- set_time = string.partition("at").last.strip!
347
- set_time = Chronic.parse(set_time)
348
- else
349
- set_time = string.partition("at").last.strip!
350
- set_time = parse_time(set_time)
351
- end
352
- end
353
-
354
- def authorize_user
355
- begin
356
- if HTTParty.get("http://#{@ip}/api/#{@user}/config").include?("whitelist") == false
357
- body = {:devicetype => "Hue_Switch", :username=>"1234567890"}
358
- create_user = HTTParty.post("http://#{@ip}/api", :body => body.to_json)
359
- puts "You need to press the link button on the bridge and run again" if create_user.first.include?("error")
360
- end
361
- rescue Errno::ECONNREFUSED
362
- puts "Cannot Reach Bridge"
363
- end
364
- end
365
-
366
- def populate_switch
367
- @colors = {red: 65280, pink: 56100, purple: 52180, violet: 47188, blue: 46920, turquoise: 31146, green: 25500, yellow: 12750, orange: 8618}
368
- @mired_colors = {candle: 500, relax: 467, reading: 346, neutral: 300, concentrate: 231, energize: 136}
369
- @scenes = [] ; HTTParty.get("http://#{@ip}/api/#{@user}/scenes").keys.each { |k| @scenes.push(k) }
370
- @groups = {} ; HTTParty.get("http://#{@ip}/api/#{@user}/groups").each { |k,v| @groups["#{v['name']}".downcase] = k } ; @groups["all"] = "0"
371
- @lights = {} ; HTTParty.get("http://#{@ip}/api/#{@user}/lights").each { |k,v| @lights["#{v['name']}".downcase] = k }
372
- end
373
-
374
- def get_bridge_by_SSDP
375
- discovered_devices = Hue.devices
376
- bridge = discovered_devices.each do |device|
377
- next unless device.get_response.include?("hue Personal")
378
- end.first
379
- end
380
- end
381
-
382
- class Device
383
- attr_reader :ip
384
- attr_reader :port
385
- attr_reader :description_url
386
- attr_reader :server
387
- attr_reader :service_type
388
- attr_reader :usn
389
- attr_reader :url_base
390
- attr_reader :name
391
- attr_reader :manufacturer
392
- attr_reader :manufacturer_url
393
- attr_reader :model_name
394
- attr_reader :model_number
395
- attr_reader :model_description
396
- attr_reader :model_url
397
- attr_reader :serial_number
398
- attr_reader :software_version
399
- attr_reader :hardware_version
400
-
401
- def initialize(info)
402
- headers = {}
403
- info[0].split("\r\n").each do |line|
404
- matches = line.match(/^([\w\-]+):(?:\s)*(.*)$/)
405
- next unless matches
406
- headers[matches[1].upcase] = matches[2]
407
- end
408
-
409
- @description_url = headers['LOCATION']
410
- @server = headers['SERVER']
411
- @service_type = headers['ST']
412
- @usn = headers['USN']
413
-
414
- info = info[1]
415
- @port = info[1]
416
- @ip = info[2]
417
- end
418
-
419
- def get_response
420
- Net::HTTP.get_response(URI.parse(description_url)).body
421
- end
422
- end
423
-
424
- class SSDP
425
- # SSDP multicast IPv4 address
426
- MULTICAST_ADDR = '239.255.255.250'.freeze
427
-
428
- # SSDP UDP port
429
- MULTICAST_PORT = 1900.freeze
430
-
431
- # Listen for all devices
432
- DEFAULT_SERVICE_TYPE = 'ssdp:all'.freeze
433
-
434
- # Timeout in 2 seconds
435
- DEFAULT_TIMEOUT = 2.freeze
436
-
437
- attr_reader :service_type
438
- attr_reader :timeout
439
- attr_reader :first
440
-
441
- # @param service_type [String] the identifier of the device you're trying to find
442
- # @param timeout [Fixnum] timeout in seconds
443
- def initialize(options = {})
444
- @service_type = options[:service_type]
445
- @timeout = (options[:timeout] || DEFAULT_TIMEOUT)
446
- @first = options[:first]
447
- initialize_socket
448
- end
449
-
450
- # Look for devices on the network
451
- def devices
452
- @socket.send(search_message, 0, MULTICAST_ADDR, MULTICAST_PORT)
453
- listen_for_responses(first)
454
- end
455
-
456
- private
457
-
458
- def listen_for_responses(first = false)
459
- @socket.send(search_message, 0, MULTICAST_ADDR, MULTICAST_PORT)
460
-
461
- devices = []
462
- Timeout::timeout(timeout) do
463
- loop do
464
- device = Device.new(@socket.recvfrom(2048))
465
- next if service_type && service_type != device.service_type
466
-
467
- if first
468
- return device
469
- else
470
- devices << device
471
- end
472
- end
473
- end
474
- devices
475
-
476
- rescue Timeout::Error => ex
477
- devices
478
- end
479
-
480
- def initialize_socket
481
- # Create a socket
482
- @socket = UDPSocket.open
483
-
484
- # We're going to use IP with the multicast TTL. Mystery third parameter is a mystery.
485
- @socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, 2)
486
- end
487
-
488
- def search_message
489
- [
490
- 'M-SEARCH * HTTP/1.1',
491
- "HOST: #{MULTICAST_ADDR}:reservedSSDPport",
492
- 'MAN: ssdp:discover',
493
- "MX: #{timeout}",
494
- "ST: #{service_type || DEFAULT_SERVICE_TYPE}"
495
- ].join("\n")
496
- end
497
- end
498
- end