lights 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZTEyNWE2YjJjODA3ZTk3Y2ZjNzRjZjk0OTY1NWI5NmFmMTMxNzU5Ng==
5
+ data.tar.gz: !binary |-
6
+ NDRlMzY3NmRmYjNmNWJlMDczNTI1MzUxN2ZjMTVlYTY4YmMyZTQ4MA==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ YmNhM2RkODM4N2I1MjQ1MTMxNzhjZDlkMmI2NGZlODUyNjQ4NDQxOTJiOGUx
10
+ YTZkMjg5YjhjZDE0ZWNjOGU4Y2E5ZDUyMmRjOGZlNThkN2ZmZjA1YjU0OTky
11
+ NTVmN2E3ODViZmQyNWY5MjQ1ZWMwYTFmYWNjZGRiNTdjN2YyYjI=
12
+ data.tar.gz: !binary |-
13
+ NWM2ODdiNTljNzhlODFiNDZhMjE4ZjlhYmNlMTFkOGE2YmY4OWRkNThkNzM4
14
+ MTgzMzllYTA0ZmIzZGVlN2QwY2E0Y2UyMTVmYThhODQ4Yzg2NGZmM2I3YzE3
15
+ ZTVmNGQwNjA2NGUzY2Y2YmU2OWMwYzRiN2Q2NDJlMjA3M2M5MGE=
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ # TESTING FILES
2
+ lib/run.rb
3
+
4
+ *.gem
5
+ *.rbc
6
+ .bundle
7
+ .config
8
+ Gemfile.lock
9
+ coverage
10
+ InstalledFiles
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+
19
+ # YARD artifacts
20
+ .yardoc
21
+ _yardoc
22
+ doc/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in lights.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Brady Turner
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,56 @@
1
+ lights
2
+ ========
3
+ A Ruby library & CLI for controlling Phillips Hue lightbulbs.
4
+
5
+ Phillips Hue API Documentation: http://developers.meethue.com/index.html
6
+
7
+ Installation
8
+ ----
9
+ ```
10
+ gem install lights
11
+ ```
12
+
13
+ Basic Usage
14
+ -----
15
+ ```ruby
16
+ require 'lights'
17
+ client = Lights.new( '192.168.x.x', 'username' )
18
+ client.register_username
19
+ client.request_bulb_list
20
+ ```
21
+
22
+ CLI Quick Setup
23
+ ----
24
+
25
+ ```
26
+ lights discover -s
27
+ lights config -f --user username
28
+ lights register
29
+ lights list
30
+ lights on -l all
31
+ lights off -l all
32
+ ```
33
+
34
+ See [Sample Usage (Implemented)](https://github.com/turnerba/lights/wiki/Sample-Usage-(Implemented)) for more usage examples.
35
+
36
+ Development
37
+ -----
38
+ #### Test:
39
+ ```
40
+ bundle exec rspec spec/
41
+ bundle exec cucumber spec/features/
42
+ ```
43
+ or
44
+ ```
45
+ rake test
46
+ ```
47
+
48
+ #### Build:
49
+ ```
50
+ rake build
51
+ ```
52
+
53
+ #### Install:
54
+ ```
55
+ rake install
56
+ ```
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ task :test do
4
+ puts `bundle exec rspec spec/`
5
+ puts `bundle exec cucumber spec/features/`
6
+ end
data/bin/lights ADDED
@@ -0,0 +1,302 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'lights'
4
+ require 'optparse'
5
+
6
+ LIGHTS_CONFIG_PATH = "#{ENV["HOME"]}/.lightsconfig"
7
+
8
+ class LightsCli
9
+
10
+ def initialize
11
+ @config = {}
12
+ if File.exists? LIGHTS_CONFIG_PATH
13
+ @config = JSON.parse( IO.read( LIGHTS_CONFIG_PATH ) )
14
+ end
15
+ end
16
+
17
+ def configured?
18
+ @config["username"] && @config["bridge_ip"]
19
+ end
20
+
21
+ def config
22
+ options = {}
23
+ OptionParser.new do |opts|
24
+ opts.on("-u", "--user <username>", String, "Username") do |url|
25
+ options[:user] = url
26
+ end
27
+ opts.on("-i", "--ip <bridge ip>", String, "Bridge ip address") do |ip|
28
+ options[:ip] = ip
29
+ end
30
+ opts.on("-f", "--force", "Force write to config file") do |f|
31
+ options[:force] = f
32
+ end
33
+ opts.on("-l", "--list", "List saved configuration settings") do |l|
34
+ options[:list] = l
35
+ end
36
+ end.parse!
37
+
38
+ if !options[:user] && !options[:ip] && !options[:list]
39
+ puts "Must specify username and/or bridge IP: --user <username> --ip <IP addr. of bridge>"
40
+ exit 1
41
+ end
42
+
43
+ if options[:list]
44
+ @config.each { |k,v| puts "#{k}: #{v}" }
45
+ else
46
+ if !options[:force] && File.exists?(LIGHTS_CONFIG_PATH)
47
+ overwrite = ""
48
+ while overwrite[0] != "y" \
49
+ && overwrite[0] != "Y" \
50
+ && overwrite[0] != "n" \
51
+ && overwrite[0] != "N" \
52
+ && overwrite[0] != "\n"
53
+ print "Lights config already exists. Overwrite? [Y/n]: "
54
+ overwrite = STDIN.gets
55
+ end
56
+ overwrite.upcase!
57
+ if overwrite[0] == "N"
58
+ exit
59
+ end
60
+ end
61
+
62
+ @config["username"] = options[:user] || @config["username"]
63
+ @config["bridge_ip"] = options[:ip] || @config["bridge_ip"]
64
+
65
+ write_config
66
+ puts "Configuration settings saved."
67
+ end
68
+ end
69
+
70
+ def list
71
+ lights = Lights.new @config["bridge_ip"], @config["username"]
72
+ if !ARGV[0] || ARGV[0] == "lights"
73
+ bulbs_response = lights.request_bulb_list
74
+ bulbs_response.each do |id,value|
75
+ bulb = lights.request_bulb_info( id )
76
+ puts "[#{id}] #{bulb.name}"
77
+ end
78
+ elsif ARGV[0] == "sensors"
79
+ sensors_response = lights.request_sensor_list
80
+ sensors_response.each do |id,value|
81
+ sensor = lights.request_sensor_info( id )
82
+ puts "[#{id}] #{sensor.name}"
83
+ end
84
+ elsif ARGV[0] == "groups"
85
+ groups_response = lights.request_group_list
86
+ groups_response.each do |id,value|
87
+ group = lights.request_group_info( id )
88
+ puts "[#{id}] #{group.name} (#{group.lights.join(',')})"
89
+ end
90
+ end
91
+ end
92
+
93
+ def register
94
+ lights = Lights.new @config["bridge_ip"], @config["username"]
95
+ response = lights.register_username
96
+ end
97
+
98
+ def discover
99
+ options = {}
100
+ OptionParser.new do |opts|
101
+ opts.on("-s", "--save", "Save discovered bridge to configuration file") do |s|
102
+ options[:save] = s
103
+ end
104
+ end.parse!
105
+
106
+ lights = Lights.new @config["bridge_ip"], @config["username"]
107
+ bridges = lights.discover_hubs
108
+ bridges.each_with_index { |b,i| puts "[#{i+1}] #{b.name}: #{b.ip}" }
109
+
110
+ if options[:save] && bridges.length >= 1
111
+ if bridges.length > 1
112
+ which_bridge = -1
113
+ while !(which_bridge >=0 && which_bridge <= bridges.length)
114
+ print "Which bridge would you like to save? (0 for none): "
115
+ which_bridge = Integer( gets ) rescue -1
116
+ end
117
+ else
118
+ which_bridge = 1
119
+ end
120
+ if which_bridge != 0
121
+ @config["bridge_ip"] = bridges[which_bridge.to_i-1].ip
122
+ write_config
123
+ puts "Discovered bridge IP saved: #{bridges[which_bridge-1].ip}"
124
+ end
125
+ elsif bridges.length == 0
126
+ puts "Did not discover any bridges."
127
+ end
128
+ end
129
+
130
+ def on
131
+ on_off true
132
+ end
133
+
134
+ def off
135
+ on_off false
136
+ end
137
+
138
+ def set
139
+ options = {}
140
+ OptionParser.new do |opts|
141
+ opts.on("-o", "--on", "Turn lights on") do |o|
142
+ options[:on] = o
143
+ end
144
+ opts.on("-f", "--off", "Turn lights off") do |f|
145
+ options[:off] = f
146
+ end
147
+ opts.on("-c", "--ct color_temp", OptionParser::DecimalInteger, "Set color temperature") do |c|
148
+ options[:ct] = c
149
+ end
150
+ opts.on("-b", "--brightness brightness", OptionParser::DecimalInteger, "Set brightness") do |b|
151
+ options[:brightness] = b
152
+ end
153
+ opts.on("-s", "--saturation saturation", OptionParser::DecimalInteger, "Set saturation") do |s|
154
+ options[:saturation] = s
155
+ end
156
+ opts.on("-h", "--hue hue", OptionParser::DecimalInteger, "Set hue") do |h|
157
+ options[:hue] = h
158
+ end
159
+ opts.on("-e", "--effect none|colorloop", String, "Set effect") do |e|
160
+ options[:effect] = e
161
+ end
162
+ opts.on("-a", "--alert none|select|lselect", String, "Set alert") do |a|
163
+ options[:alert] = a
164
+ end
165
+ opts.on("-z", "--xy x,y", Array, "Set xy") do |z|
166
+ options[:xy] = z
167
+ end
168
+ opts.on("-l", "--lights 1,2,...N", Array, "Which lights to control") do |l|
169
+ options[:lights] = l
170
+ end
171
+ opts.on("-d", "--duration seconds", OptionParser::DecimalInteger, "Transition duration in seconds") do |d|
172
+ options[:duration] = d
173
+ end
174
+ end.parse!
175
+
176
+ bad_args = false
177
+ if !options[:on] && !options[:off] \
178
+ && !options[:ct] && !options[:brightness] \
179
+ && !options[:hue] && !options[:saturation] \
180
+ && !options[:effect] && !options[:alert] \
181
+ && !options[:xy]
182
+ puts "Must specify a state to set."
183
+ bad_args = true
184
+ end
185
+ if !options[:lights]
186
+ puts "Must specify which lights to control."
187
+ bad_args = true
188
+ end
189
+ if (options[:hue] || options[:saturation]) \
190
+ && options[:ct]
191
+ puts "Cannot set both color temperature and hue/saturation."
192
+ bad_args = true
193
+ end
194
+ exit 1 if bad_args
195
+
196
+ s = BulbState.new
197
+ if options[:on]
198
+ s.on = true
199
+ elsif options[:off]
200
+ s.on = false
201
+ end
202
+ if options[:ct]
203
+ s.ct = options[:ct]
204
+ end
205
+ if options[:brightness]
206
+ s.bri = options[:brightness]
207
+ end
208
+ if options[:saturation]
209
+ s.sat = options[:saturation]
210
+ end
211
+ if options[:hue]
212
+ s.hue = options[:hue]
213
+ end
214
+ if options[:effect]
215
+ s.effect = options[:effect]
216
+ end
217
+ if options[:duration]
218
+ s.transition_time = options[:duration] * 10
219
+ end
220
+ if options[:alert]
221
+ s.alert = options[:alert]
222
+ end
223
+ if options[:xy]
224
+ s.xy = options[:xy]
225
+ end
226
+ set_state(s,options[:lights])
227
+
228
+ end
229
+
230
+ private
231
+ def set_state(state,bulbs)
232
+ lights = Lights.new @config["bridge_ip"], @config["username"]
233
+ if bulbs.first == "all"
234
+ lights.set_group_state 0,state
235
+ else
236
+ bulbs.each { |l| lights.set_bulb_state(l,state) }
237
+ end
238
+ end
239
+
240
+ def write_config
241
+ File.open(LIGHTS_CONFIG_PATH,"w+") { |file| file.write(@config.to_json) }
242
+ end
243
+
244
+ def on_off(is_on)
245
+ options = {}
246
+ OptionParser.new do |opts|
247
+ opts.on("-l", "--lights 1,2,...N", Array, "Which lights to control") do |l|
248
+ options[:lights] = l
249
+ end
250
+ end.parse!
251
+
252
+ if !options[:lights]
253
+ puts "Must specify which lights to control."
254
+ exit 1
255
+ end
256
+
257
+ s = BulbState.new
258
+ s.on = is_on
259
+ set_state s, options[:lights]
260
+ end
261
+ end
262
+
263
+
264
+ if !ARGV[0]
265
+ STDERR.puts "Must specify a command. (config, list, register, discover, on, off, set)"
266
+ exit 1
267
+ end
268
+
269
+ begin
270
+ cli = LightsCli.new
271
+ command = ARGV.shift
272
+ if command == "config"
273
+ cli.config
274
+ elsif command == "discover"
275
+ cli.discover
276
+ elsif !cli.configured?
277
+ puts "Please run 'lights config' before using."
278
+ elsif command == "list"
279
+ cli.list
280
+ elsif command == "register"
281
+ cli.register
282
+ elsif command == "on"
283
+ cli.on
284
+ elsif command == "off"
285
+ cli.off
286
+ elsif command == "set"
287
+ cli.set
288
+ else
289
+ puts "Cannot find command #{command}."
290
+ end
291
+ rescue BridgeConnectException,
292
+ UsernameException,
293
+ BulbStateValueOutOfRangeException,
294
+ BulbStateValueTypeException => e
295
+ puts e.message
296
+ rescue Errno::ENETUNREACH, Errno::ENETDOWN
297
+ puts "Please check your internet connection and try again."
298
+ rescue Interrupt
299
+ puts ""
300
+ end
301
+
302
+
data/lib/lights.rb ADDED
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'json'
6
+
7
+ require 'lights/bulb.rb'
8
+ require 'lights/group.rb'
9
+ require 'lights/bridge.rb'
10
+ require 'lights/exception.rb'
11
+ require 'lights/sensor.rb'
12
+ require 'lights/loggerconfig.rb'
13
+
14
+ def jp( s )
15
+ puts JSON.pretty_generate( s )
16
+ end
17
+
18
+ class Lights
19
+
20
+ attr_reader :bulbs
21
+ def initialize(ip,username)
22
+ @ip = ip
23
+ @username = username
24
+ @http = Net::HTTP.new(ip,80)
25
+ @bulbs = []
26
+ @groups = []
27
+ @bridges = []
28
+ @logger = Logger.new(STDERR)
29
+ @logger.level = LoggerConfig::LIGHTS_LEVEL
30
+ end
31
+
32
+ def discover_hubs
33
+ http = Net::HTTP.new("www.meethue.com",443)
34
+ http.use_ssl = tlights
35
+ request = Net::HTTP::Get.new( "/api/nupnp" )
36
+ response = http.request request
37
+
38
+ case response.code.to_i
39
+ when 200
40
+ result = JSON.parse( response.body )
41
+ result.each { |b| @bridges << Bridge.new(b) }
42
+ else
43
+ raise "Unknown error"
44
+ end
45
+ @bridges
46
+ end
47
+
48
+ def register_username
49
+ data = { "devicetype"=>"lights",
50
+ "username"=>@username }
51
+ response = @http.post "/api", data.to_json
52
+ result = JSON.parse(response.body).first
53
+ if result.has_key? "error"
54
+ process_error result
55
+ end
56
+ end
57
+
58
+ def request_config
59
+ get "config"
60
+ end
61
+
62
+ def add_bulb(id,bulb_data)
63
+ @bulbs << Bulb.new( id, bulb_data )
64
+ end
65
+
66
+ def request_bulb_list
67
+ get "lights"
68
+ end
69
+
70
+ def request_bulb_info( id )
71
+ response = get "lights/#{id}"
72
+ Bulb.new(id,response)
73
+ end
74
+
75
+ def request_group_info( id )
76
+ response = get "groups/#{id}"
77
+ Group.new(id,response)
78
+ end
79
+
80
+ def request_sensor_info( id )
81
+ response = get("sensors/#{id}")
82
+ Sensor.new(id,response)
83
+ end
84
+
85
+ def request_sensor_list
86
+ get "sensors"
87
+ end
88
+
89
+ def request_group_list
90
+ get "groups"
91
+ end
92
+
93
+ def request_schedule_list
94
+ get "schedules"
95
+ end
96
+
97
+ def set_bulb_state( id, state )
98
+ put "lights/#{id}/state", state
99
+ end
100
+
101
+ def set_group_state( id, state )
102
+ put "groups/#{id}/action", state
103
+ end
104
+
105
+ private
106
+
107
+ def process_error(result)
108
+ type = result["error"]["type"]
109
+ case type
110
+ when 1
111
+ raise UsernameException
112
+ when 101
113
+ raise BridgeConnectException
114
+ else
115
+ puts "Unknown Error."
116
+ end
117
+ end
118
+
119
+ def get( path )
120
+ @logger.debug "==> GET: #{path}"
121
+ request = Net::HTTP::Get.new( "/api/#{@username}/#{path}" )
122
+ response = @http.request request
123
+ result = JSON.parse( response.body )
124
+ @logger.debug "<== #{response.code}"
125
+ @logger.debug response.body
126
+ if result.first.kind_of?(Hash) && result.first.has_key?("error")
127
+ process_error result.first
128
+ end
129
+ result
130
+ end
131
+
132
+ def put( path, data )
133
+ @logger.debug "==> PUT: #{path}"
134
+ @logger.debug data.to_json
135
+ response = @http.put( "/api/#{@username}/#{path}", data.to_json )
136
+ @logger.debug "<== #{response.code}"
137
+ @logger.debug response.body
138
+ JSON.parse response.body
139
+ end
140
+
141
+ end
142
+