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 +4 -4
- data/Gemfile +1 -0
- data/alexa_hue.gemspec +2 -1
- data/lib/alexa_hue/hue/client.rb +216 -0
- data/lib/alexa_hue/hue/helpers.rb +62 -0
- data/lib/alexa_hue/hue/js_client.rb +66 -0
- data/lib/alexa_hue/hue/request_body.rb +29 -0
- data/lib/alexa_hue/hue/voice_parser.rb +80 -0
- data/lib/alexa_hue/version.rb +1 -1
- data/lib/alexa_hue.rb +29 -40
- metadata +23 -7
- data/.DS_Store +0 -0
- data/lib/alexa_hue/fix_schedule_syntax.rb +0 -19
- data/lib/alexa_hue/hue_switch.rb +0 -498
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 966f685e91dc2b39bca4fe3460b3e54dcf9ad350
|
4
|
+
data.tar.gz: 3695c7c36fe1ca51430e386661d54c0ee23e0051
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7c57e8457a958dddf91067cc04adf62f0927180fe9fd82acc02bfa56b6b50ad8ec34243e7f6faf7f0c734fb9235b17d42576ca5f846af3b8643f8cb069d317fc
|
7
|
+
data.tar.gz: af8afc1312c35e9910965bea816dbc7f57178b7642c59243855c0e811f85ad276d8ac59959b5b6d31b84a2118ca4fa58e750b9ce5851031b30cf5feebac75b79
|
data/Gemfile
CHANGED
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 '
|
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
|
data/lib/alexa_hue/version.rb
CHANGED
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/
|
8
|
+
require 'sinatra/extension'
|
9
9
|
require 'numbers_in_words/duck_punch'
|
10
|
-
require 'alexa_hue/
|
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
|
-
|
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
|
-
|
28
|
-
|
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
|
-
|
52
|
-
|
53
|
-
|
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.
|
58
|
-
|
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 !(
|
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 !(
|
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
|
-
#
|
81
|
-
|
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.
|
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-
|
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:
|
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/
|
211
|
-
- lib/alexa_hue/
|
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.
|
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
|
data/lib/alexa_hue/hue_switch.rb
DELETED
@@ -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
|