lights 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,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
+