ruhue 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source :rubygems
2
+
3
+ gemspec
4
+
5
+ gem 'pry'
6
+ gem 'yard'
7
+ gem 'redcarpet'
data/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # Ruhue
2
+
3
+ So far, mere documentations of my findings when sniffing the Hue.
4
+
5
+ There is a mailing list, dedicated to discussions and questions about hacking the
6
+ Philips Hue and related protocols.
7
+
8
+ - Mailing list web interface: <https://groups.google.com/d/forum/hue-hackers>
9
+ - Mailing list e-mail address: <hue-hackers@googlegroups.com>
10
+
11
+ Other link resources:
12
+
13
+ - [Hack the Hue](http://rsmck.co.uk/hue)
14
+ - [A Day with Philips Hue](http://www.nerdblog.com/2012/10/a-day-with-philips-hue.html?showComment=1352172383498)
15
+
16
+ ## Console
17
+
18
+ There is a console script in this repository, written by @Burgestrand as the
19
+ documentation effort travels further. It is written in Ruby, and only supports
20
+ Ruby 1.9.x and newer. You may start the console with the following:
21
+
22
+ 1. Install bundler: `gem install bundler`
23
+ 2. Install console script dependencies: `bundle install`
24
+ 3. Run the console script: `ruby console.rb`
25
+
26
+ You’ll be dropped into a pry prompt (similar to IRB), with access to the following
27
+ local variables:
28
+
29
+ - hue — a Hue instance, documented in `lib/hue.rb`
30
+ - client — a Hue::Client, documented in `lib/hue/client.rb`
31
+
32
+ Once the Hue API exploration adventure starts slowing down, the scripts will be
33
+ turned into a ruby gem and tested with rspec.
34
+
35
+ ## API
36
+
37
+ The API reference documentation has moved to the [hue-api][] repository. It is
38
+ hosted on <http://burgestrand.github.com/hue-api/> using GitHub Pages. Contributions
39
+ are very welcome, and if you’d like commit access to the hue-api repository, just ask!
40
+
41
+ [hue-api]: https://github.com/Burgestrand/hue-api
42
+
43
+ ## Gem License
44
+
45
+ Copyright (c) 2012 Kim Burgestrand
46
+
47
+ MIT License
48
+
49
+ Permission is hereby granted, free of charge, to any person obtaining
50
+ a copy of this software and associated documentation files (the
51
+ "Software"), to deal in the Software without restriction, including
52
+ without limitation the rights to use, copy, modify, merge, publish,
53
+ distribute, sublicense, and/or sell copies of the Software, and to
54
+ permit persons to whom the Software is furnished to do so, subject to
55
+ the following conditions:
56
+
57
+ The above copyright notice and this permission notice shall be
58
+ included in all copies or substantial portions of the Software.
59
+
60
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
61
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
62
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
63
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
64
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
65
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
66
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ begin
2
+ require "bundler/gem_tasks"
3
+ rescue LoadError
4
+ # Not everybody needs bundler tasks.
5
+ end
6
+
7
+ begin
8
+ require 'yard'
9
+ YARD::Rake::YardocTask.new('yard:doc') do |task|
10
+ task.options = ['--no-stats']
11
+ end
12
+
13
+ task 'yard:stats' do
14
+ YARD::CLI::Stats.run('--list-undoc')
15
+ end
16
+
17
+ task :yard => ['yard:doc', 'yard:stats']
18
+ rescue LoadError
19
+ puts "WARN: YARD not available. You may install documentation dependencies via bundler."
20
+ end
21
+
22
+ desc "Run an interactive console with Ruhue loaded"
23
+ task :console do
24
+ exec "irb", "-Ilib", "-rruhue"
25
+ end
26
+
27
+ require 'rspec/core/rake_task'
28
+ RSpec::Core::RakeTask.new do |spec|
29
+ spec.ruby_opts = %w[-W]
30
+ end
31
+
32
+ task :default => :spec
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+ require "bundler/setup"
4
+ require "pry"
5
+ require "ruhue"
6
+
7
+ def prompt(query)
8
+ print(query)
9
+ gets.chomp.tap { puts }
10
+ end
11
+
12
+ puts <<-HELLO
13
+ Hi there!
14
+
15
+ This is an interpreter to aid experimenting with the Hue. It’s written in Ruby
16
+ and makes API calls to the Hue hub a little more pleasant. You’re expected to
17
+ know Ruby in order to use this.
18
+
19
+ I’ll first ask you for your Hue hub username. If you do not have one, just make
20
+ one up and I’ll register an application for you on the Hue hub. Use the same
21
+ username every time, or you’ll have a lot of unused applications registered to
22
+ your Hue after a little while.
23
+
24
+ First off, I’ll see if I can find your Hue. Give me five seconds at most.
25
+ HELLO
26
+
27
+ hue = Ruhue.discover
28
+ puts
29
+ puts "Hue discovered at #{hue.host}!"
30
+ puts
31
+
32
+ username = prompt("Now, your username please (10-40 characters, 0-9, a-z, A-Z): ")
33
+ client = Ruhue::Client.new(hue, username)
34
+
35
+ puts "Hi #{client.username}!"
36
+
37
+ if client.registered?
38
+ puts "It appears you’re already registered with the Hub. Play away!"
39
+ elsif client.register("ruhue")
40
+ puts "A new application has been registered with the Hub as #{client.username}. Play away!"
41
+ end
42
+
43
+ # Blinks all your lights!
44
+ #
45
+ # num_lights = 3
46
+ # on = true
47
+ # 10_000.times do |i|
48
+ # sleep(0.04)
49
+ # light = (i%num_lights) + 1
50
+ # client.put("/lights/#{light}/state", on: on)
51
+ # on = ! on if light == num_lights
52
+ # end
53
+
54
+ binding.pry
data/lib/ruhue.rb ADDED
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ require 'httpi'
5
+ require 'nokogiri'
6
+ require 'socket'
7
+ require 'timeout'
8
+ require 'json'
9
+
10
+ class Ruhue
11
+ TimeoutError = Class.new(TimeoutError)
12
+ APIError = Class.new(StandardError)
13
+
14
+ class << self
15
+ # Search for a Hue hub, with configurable timeout.
16
+ #
17
+ # This sends out a broadcast packet with UDP, and waits for
18
+ # a response from the Hue hub.
19
+ #
20
+ # @param [Integer] timeout seconds until giving up
21
+ # @raise [Ruhue::TimeoutError] in case timeout is reached
22
+ # @return [Hub]
23
+ def discover(timeout = 5)
24
+ socket = UDPSocket.new(Socket::AF_INET)
25
+ payload = []
26
+ payload << "M-SEARCH * HTTP/1.1"
27
+ payload << "HOST: 239.255.255.250:1900"
28
+ payload << "MAN: ssdp:discover"
29
+ payload << "MX: 10"
30
+ payload << "ST: ssdp:all"
31
+ socket.send(payload.join("\n"), 0, "239.255.255.250", 1900)
32
+
33
+ Timeout.timeout(timeout, Ruhue::TimeoutError) do
34
+ loop do
35
+ message, (_, _, hue_ip, _) = socket.recvfrom(1024)
36
+ # TODO: improve this. How do we know it’s a Hue hub?
37
+ return new(hue_ip) if message =~ /description\.xml/
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ # @example
44
+ # hue = Ruhue.new("192.168.0.21")
45
+ #
46
+ # @param [String] host address to the Hue hub.
47
+ def initialize(host)
48
+ @host = host.to_str
49
+ end
50
+
51
+ # @return [String] hue host
52
+ attr_reader :host
53
+
54
+ # @param [String] path
55
+ # @return [String] full url
56
+ def url(path)
57
+ "http://#{host}/#{path.to_s.sub(/\A\//, "")}"
58
+ end
59
+
60
+ # GET a path of the Hue.
61
+ #
62
+ # @param [String] path
63
+ # @return [Ruhue::Response]
64
+ def get(path)
65
+ request(:get, path)
66
+ end
67
+
68
+ # POST a payload to the Hue.
69
+ #
70
+ # @param [String] path
71
+ # @param data json-serializable
72
+ # @return [Ruhue::Response]
73
+ def post(path, data)
74
+ request(:post, path, JSON.dump(data))
75
+ end
76
+
77
+ # PUT a payload to the Hue.
78
+ #
79
+ # @param [String] path
80
+ # @param data json-serializable
81
+ # @return [Ruhue::Response]
82
+ def put(path, data)
83
+ request(:put, path, JSON.dump(data))
84
+ end
85
+
86
+ # DELETE a resource.
87
+ #
88
+ # @param [String] path
89
+ # @return [Ruhue::Response]
90
+ def delete(path)
91
+ request(:delete, path)
92
+ end
93
+
94
+ # @return [Nokogiri::XML] Hue device description
95
+ def description
96
+ Nokogiri::XML(get("/description.xml").body)
97
+ end
98
+
99
+ protected
100
+
101
+ def request(method, path, *args)
102
+ response = HTTPI.send(method, url(path), *args)
103
+ Ruhue::Response.new(response)
104
+ end
105
+ end
106
+
107
+ require 'ruhue/response'
108
+ require 'ruhue/client'
@@ -0,0 +1,61 @@
1
+ class Ruhue::Client
2
+ class << self
3
+ def valid_username?(name)
4
+ name =~ /\A[0-9a-zA-Z]{10,40}\z/
5
+ end
6
+ end
7
+
8
+ # Create a Hue client. You’ll need a Hue hub
9
+ # username for this, created via a POST to the
10
+ # /api endpoint. See README for more information.
11
+ #
12
+ # @param [Ruhue] hue
13
+ # @param [String] username
14
+ def initialize(hue, username)
15
+ unless self.class.valid_username?(username)
16
+ raise ArgumentError, "invalid username, must be length 10-40, only numbers and letters"
17
+ end
18
+
19
+ @hue = hue
20
+ @username = username
21
+ end
22
+
23
+ # @return [Ruhue]
24
+ attr_reader :hue
25
+
26
+ # @return [String]
27
+ attr_reader :username
28
+
29
+ # Register a given username with the Hue hub.
30
+ #
31
+ # @param [String] device_type used as device name
32
+ # @return [Ruhue::Client] if successful
33
+ # @raise [APIError] if failed
34
+ def register(device_type)
35
+ response = hue.post("/api", username: username, devicetype: device_type)
36
+ tap { raise Ruhue::APIError, response.error_messages.join(", ") if response.error? }
37
+ end
38
+
39
+ # @return [Boolean] true if username is registered.
40
+ def registered?
41
+ not get("/").error?
42
+ end
43
+
44
+ def get(path)
45
+ hue.get(url(path))
46
+ end
47
+
48
+ def post(path, data)
49
+ hue.post(url(path), data)
50
+ end
51
+
52
+ def put(path, data)
53
+ hue.put(url(path), data)
54
+ end
55
+
56
+ protected
57
+
58
+ def url(path)
59
+ "/api/#{username}/#{path.sub(/\A\//, "")}"
60
+ end
61
+ end
@@ -0,0 +1,23 @@
1
+ class Ruhue::Response
2
+ # @param [HTTPI::Response] response
3
+ def initialize(response)
4
+ @response = response
5
+ @data = JSON.load(response.body)
6
+ end
7
+
8
+ # @return [String] body
9
+ attr_reader :response
10
+
11
+ # @return [Array, Hash] data
12
+ attr_reader :data
13
+
14
+ # @return [Boolean] true if the response is an error.
15
+ def error?
16
+ data.is_a?(Array) and data.any? { |hash| hash.has_key?("error") }
17
+ end
18
+
19
+ # @return [Array<String>, nil] array of error messages and their address, nil if no error.
20
+ def error_messages
21
+ data.map { |hash| "#{hash["error"]["address"]}: #{hash["error"]["description"]}" } if error?
22
+ end
23
+ end
@@ -0,0 +1,4 @@
1
+ class Ruhue
2
+ # @see http://semver.org/
3
+ VERSION = "0.1.0"
4
+ end
data/ruhue.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ruhue/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "ruhue"
8
+ gem.summary = %q{API client for interacting with the Philips Hue Hub HTTP API.}
9
+
10
+ gem.homepage = "https://github.com/Burgestrand/ruhue"
11
+ gem.authors = ["Kim Burgestrand"]
12
+ gem.email = ["kim@burgestrand.se"]
13
+ gem.license = "MIT License"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.version = Ruhue::VERSION
21
+
22
+ gem.add_dependency 'httpi'
23
+ gem.add_dependency 'nokogiri'
24
+ gem.add_development_dependency 'rspec'
25
+ end
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruhue
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Kim Burgestrand
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-09 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: httpi
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: nokogiri
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ description:
63
+ email:
64
+ - kim@burgestrand.se
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - .gitignore
70
+ - Gemfile
71
+ - Gemfile.lock
72
+ - README.md
73
+ - Rakefile
74
+ - examples/console.rb
75
+ - lib/ruhue.rb
76
+ - lib/ruhue/client.rb
77
+ - lib/ruhue/response.rb
78
+ - lib/ruhue/version.rb
79
+ - ruhue.gemspec
80
+ homepage: https://github.com/Burgestrand/ruhue
81
+ licenses:
82
+ - MIT License
83
+ post_install_message:
84
+ rdoc_options: []
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ! '>='
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ! '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ requirements: []
100
+ rubyforge_project:
101
+ rubygems_version: 1.8.24
102
+ signing_key:
103
+ specification_version: 3
104
+ summary: API client for interacting with the Philips Hue Hub HTTP API.
105
+ test_files: []
106
+ has_rdoc: