alexa_hue 1.1.0 → 1.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.
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