hue-lib 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ Gemfile.lock
2
+ *.gem
data/README.md CHANGED
@@ -1,7 +1,8 @@
1
- RubyHue
1
+ hue-lib
2
2
  ================
3
3
 
4
- This is a very early attempt to create a Ruby library for controlling the [Philips Hue](http://www.meethue.com) lighting system. The API has not yet been released, but there are [several](http://www.nerdblog.com/2012/10/a-day-with-philips-hue.html) [people](http://rsmck.co.uk/hue) working to figure it out.
4
+ This is a Ruby library for controlling the [Philips Hue](http://www.meethue.com) lighting system.
5
+ The API has not yet been released, but there are [several](http://www.nerdblog.com/2012/10/a-day-with-philips-hue.html) [people](http://rsmck.co.uk/hue) working to figure it out.
5
6
 
6
7
  # WARNING
7
8
  All of this is very experimental and could permanently damage your awesome (but ridiculously expensive) lightbulbs. As such, exercise extreme caution.
@@ -9,30 +10,49 @@ All of this is very experimental and could permanently damage your awesome (but
9
10
  ## Getting Started
10
11
  You can get a [great overview](http://rsmck.co.uk/hue) of the options and limitations of the lights from Ross McKillop.
11
12
 
12
- You will need to find the IP address of your bridge unit and also generate a unique ID (UUID works great) for your controlling application and add them to the top of the `hue.rb` file.
13
-
14
- You will need to use this information to register your app with the controller. This library does not do that at this time, so you will need to manually do that. I suggest following the *Registering Your Application* section of [Ross's overview](http://rsmck.co.uk/hue).
15
-
16
13
  ## Usage
17
- To begin using, fire up the irb console and load the `hue.rb` file.
14
+ To begin using, fire up the irb console from the project root thus:
15
+
16
+ ```
17
+ irb -I lib
18
+ ```
18
19
 
19
20
  ```ruby
20
- >> load 'hue.rb'
21
+ >> require 'hue'
21
22
  => true
22
23
  ```
23
24
 
25
+ Start by registering your application. Press the button on the bridge and execute
26
+
27
+ ```ruby
28
+ >> Hue.register_default
29
+ => #<Hue::Bridge:0x8b9d950 @application_id="4aa41fe737808af3559f3d22ca67a0ca", @base_uri="http://198.168.1.1/api">
30
+ ```
31
+
32
+ This will create two config files in your ~/.hue-lib directory.
33
+ One for the bridges discovered on your network and one for the registered application.
34
+
35
+ You can fetch the default application thus:
36
+
37
+ ```ruby
38
+ >> bridge = Hue.application
39
+ => #<Hue::Bridge:0x8b9d950 @application_id="4aa41fe737808af3559f3d22ca67a0ca", @base_uri="http://198.168.1.1/api">
40
+ ```
41
+
24
42
  You can see all of the lights attached to your controller by querying the bridge.
25
43
 
26
44
  ```ruby
27
- >> Hue::Bridge.identities
28
- => {"1"=>"Master Bedroom Dresser", "2"=>"Wife Bedside", "3"=>"Bedside (front)", "4"=>"Bedside (back)", "5"=>"Family Room Desk", "6"=>"Family Room", "7"=>"Living Room Square"}
45
+ >> bridge.lights
46
+ => {"1"=>{"name"=>"Bedroom Overhead"}, "2"=>{"name"=>"Living Overhead"}, "3"=>{"name"=>"Standing Lamp"}, "4"=>{"name"=>"Living Cabinet"}}
47
+ >> bridge.light_names
48
+ => "1. Bedroom Overhead\n2. Living Overhead\n3. Standing Lamp\n4. Living Cabinet"
29
49
  ```
30
50
 
31
51
  If you know the ID number of a particular lamp, you can access it directly.
32
52
 
33
53
  ```ruby
34
- >> b = Hue::Bulb.new(5)
35
- => #<Hue::Bulb:0x007fe35a3586b8 @id=5, @hub=#<Hue::Bridge:0x007fe35a358690 @light_id="5">>
54
+ >> b = Hue::Bulb.new(bridge, 1)
55
+ => #<Hue::Bulb:0x007fe35a3586b8 @bridge=#<Hue::Bridge:0x007fe35a358690 @id="1">>
36
56
 
37
57
  # on/off
38
58
  >> b.on?
@@ -62,27 +82,12 @@ If you know the ID number of a particular lamp, you can access it directly.
62
82
  => false
63
83
 
64
84
  >> b.blink
65
- => nil
66
85
 
67
86
  >> b.blinking?
68
87
  => true
69
88
 
70
- >> b.blink false
71
- => nil
89
+ >> b.solid
72
90
 
73
91
  >> b.blinking?
74
92
  => false
75
93
  ```
76
-
77
- ## Experimental
78
- There is an experimental mode that attempts to simulate a candle flicker. This defaults to only flickering 15 times as it's really not the way the bridge or bulbs were designed to work. Additionally, this operates on the main thread so you really can't do anything else while it's running.
79
-
80
- ```ruby
81
- >> b.candle
82
- => nil
83
- ```
84
-
85
- The candle makes use of temporarily stashing the lamp's current settings before it starts and then restoring them upon completion. You can use this yourself with the `stash` and `restore` commands.
86
-
87
- ## Going Forward
88
- There is still a lot of work to be done figuring out the various timer options of the hub, etc. Hopefully, the official API will be released in the near future and expose even more goodies that we're unaware of.
data/hue-lib.gemspec CHANGED
@@ -3,8 +3,8 @@ $:.push File.expand_path("../lib", __FILE__)
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "hue-lib"
6
- s.version = '0.5.0'
7
- s.authors = ["Birkir A. Barkarson", ""]
6
+ s.version = '0.6.0'
7
+ s.authors = ["Birkir A. Barkarson", "Aaron Hurley"]
8
8
  s.email = ["birkirb@stoicviking.net"]
9
9
  s.homepage = "https://github.com/birkirb/hue-lib"
10
10
  s.summary = %q{Ruby library for controlling Phillips Hue light bridge.}
data/lib/hue/bridge.rb CHANGED
@@ -1,52 +1,16 @@
1
- require 'digest/md5'
2
- require 'uuid'
3
- # require 'singleton'
1
+ require 'socket'
2
+ require 'timeout'
4
3
 
5
4
  module Hue
6
5
  class Bridge
7
- # include Singleton
8
-
9
- # # Remove
10
- # def self.method_missing(method, *args, &block)
11
- # if args.empty?
12
- # self.instance.send method
13
- # else
14
- # self.instance.send method, *args
15
- # end
16
- # end
17
-
18
- # Move to APP class
19
- def self.register(host = BASE)
20
- # TODO: Look for default config.
21
- puts "Please press the button on bridge before continuing."
22
- puts "Once done, press Enter to continue."
23
- gets
24
- secret = Digest::MD5.hexdigest(UUID.generate) # one time UUID
25
- puts "Registering app...(#{secret})"
26
- config = Hue::Config.new(host, secret)
27
- instance.create(
28
- URI.parse(config.base_uri),
29
- {"username" => config.base_uri, "devicetype" => Hue.device_type}
30
- )
31
- config.write
32
- end
33
-
34
- # Move to APP class
35
- def self.remove
36
- config = Config.default
37
- instance.delete(
38
- URI.parse(config.base_uri),
39
- {"username" => config.identifier}
40
- )
41
- config.delete
42
- end
43
6
 
44
7
  public
45
8
 
46
- attr_reader :hue_config
9
+ attr_reader :application_id, :bridge_uri
47
10
 
48
- def initialize(hue_config = Hue.config)
49
- @hue_config = hue_config
11
+ def initialize(application_id, bridge_uri)
12
+ @application_id = application_id
13
+ @bridge_uri = bridge_uri
50
14
  end
51
15
 
52
16
  def status
@@ -93,42 +57,55 @@ module Hue
93
57
  update(uri('lights', id, 'state'), state)
94
58
  end
95
59
 
60
+ def register
61
+ create(URI.parse(bridge_uri),
62
+ {"username" => application_id, "devicetype" => Hue.device_type})
63
+ end
64
+
65
+ def unregister
66
+ delete(uri('config', 'whitelist', application_id))
67
+ end
68
+
96
69
  private
97
70
 
98
71
  def uri(*args)
99
- URI [hue_config.base_uri, hue_config.identifier, args].flatten.reject{|x| x.to_s.strip == ''}.join('/')
72
+ URI.parse([bridge_uri, application_id, args].flatten.reject { |x| x.to_s.strip == '' }.join('/'))
100
73
  end
101
74
 
102
75
  def index(url)
103
- # json = Net::HTTP.get(url)
104
- # JSON.parse(json)
105
76
  request = Net::HTTP::Get.new(url.request_uri, initheader = {'Content-Type' =>'application/json'})
106
- response = Net::HTTP.new(url.host, url.port).start {|http| http.request(request) }
107
- display(response)
108
- json = JSON.parse(response.body)
109
- if json.is_a?(Array) && error = json.first['error']
110
- raise Hue::API::Error.new(error)
111
- else
112
- json
113
- end
77
+ parse_and_check_response(Net::HTTP.new(url.host, url.port).start { |http| http.request(request) })
114
78
  end
115
79
 
116
80
  def update(url, settings = {})
117
81
  request = Net::HTTP::Put.new(url.request_uri, initheader = {'Content-Type' =>'application/json'})
118
82
  request.body = settings.to_json
119
- display Net::HTTP.new(url.host, url.port).start {|http| http.request(request) }
83
+ parse_and_check_response(Net::HTTP.new(url.host, url.port).start { |http| http.request(request) })
120
84
  end
121
85
 
122
86
  def delete(url, settings = {})
123
87
  request = Net::HTTP::Delete.new(url.request_uri, initheader = {'Content-Type' =>'application/json'})
124
88
  request.body = settings.to_json
125
- display Net::HTTP.new(url.host, url.port).start{|http| http.request(request)}
89
+ parse_and_check_response(Net::HTTP.new(url.host, url.port).start{ |http| http.request(request) })
126
90
  end
127
91
 
128
92
  def create(url, settings = {})
129
93
  request = Net::HTTP::Post.new(url.request_uri, initheader = {'Content-Type' =>'application/json'})
130
94
  request.body = settings.to_json
131
- display Net::HTTP.new(url.host, url.port).start {|http| http.request(request) }
95
+ parse_and_check_response(Net::HTTP.new(url.host, url.port).start { |http| http.request(request) })
96
+ end
97
+
98
+ def parse_and_check_response(response)
99
+ if display(response)
100
+ json = JSON.parse(response.body)
101
+ if json.is_a?(Array) && error = json.first['error']
102
+ raise Hue::API::Error.new(error)
103
+ else
104
+ json
105
+ end
106
+ else
107
+ raise Hue::Error.new("Unexpected response: #{response.code}, #{response.message}")
108
+ end
132
109
  end
133
110
 
134
111
  def display(response = nil)
@@ -0,0 +1,73 @@
1
+ require 'yaml'
2
+ require 'fileutils'
3
+
4
+ module Hue
5
+ module Config
6
+ class NotFound < Hue::Error; end;
7
+
8
+ class Abstract
9
+
10
+ public
11
+
12
+ attr_reader :path, :name
13
+
14
+ def initialize(name, path)
15
+ @path = path
16
+ @name = name
17
+ self.class.setup_file_path(self.path)
18
+ end
19
+
20
+ def write(overwrite_existing_key = false)
21
+ yaml = YAML.load_file(self.path) rescue Hash.new
22
+ if yaml.key?(name) && !overwrite_existing_key
23
+ raise "Key named '#{name}' already exists in config file '#{self.path}'.\nPlease remove it before creating a new one with the same name."
24
+ else
25
+ add_self_to_yaml(yaml)
26
+ dump_yaml(yaml)
27
+ end
28
+ end
29
+
30
+ def delete
31
+ yaml = YAML.load_file(self.path) rescue Hash::New
32
+
33
+ if yaml.key?(name)
34
+ yaml.delete(name)
35
+ end
36
+
37
+ dump_yaml(yaml)
38
+ end
39
+
40
+ def ==(rhs)
41
+ lhs = self
42
+
43
+ lhs.class == rhs.class && lhs.name == rhs.name
44
+ end
45
+
46
+ private
47
+
48
+ def add_self_to_yaml(yaml)
49
+ yaml[name] = {}
50
+ end
51
+
52
+ def dump_yaml(yaml)
53
+ File.open(self.path, 'w+' ) do |out|
54
+ YAML.dump(yaml, out)
55
+ end
56
+ end
57
+
58
+ def self.setup_file_path(path)
59
+ dir = File.dirname(path)
60
+ FileUtils.mkdir_p(dir) unless Dir.exists?(dir)
61
+ end
62
+
63
+ def self.read_file(config_file)
64
+ begin
65
+ yaml = YAML.load_file(config_file)
66
+ rescue => err
67
+ raise Error.new("Failed to read configuration file", err)
68
+ end
69
+ end
70
+
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,55 @@
1
+ module Hue
2
+ module Config
3
+ class Application < Abstract
4
+
5
+ STRING_BRIDGE_ID = 'bridge_id'
6
+ STRING_DEFAULT = 'default'
7
+ STRING_ID = 'id'
8
+
9
+ def self.file_path
10
+ File.join(ENV['HOME'], ".#{Hue.device_type}", 'applications.yml')
11
+ end
12
+
13
+ def self.default
14
+ named(STRING_DEFAULT)
15
+ end
16
+
17
+ def self.named(name)
18
+ yaml = read_file(file_path)
19
+ if named_yaml = yaml[name]
20
+ new(named_yaml[STRING_BRIDGE_ID], named_yaml[STRING_ID], name)
21
+ else
22
+ raise NotFound.new("Config named '#{name}' not found.")
23
+ end
24
+ end
25
+
26
+ public
27
+
28
+ attr_reader :bridge_id, :id, :name
29
+
30
+ def initialize(bridge_id, id, name = STRING_DEFAULT, path = self.class.file_path)
31
+ super(name, path)
32
+ @bridge_id = bridge_id
33
+ @id = id
34
+ end
35
+
36
+ def ==(rhs)
37
+ super(rhs) &&
38
+ self.bridge_id == rhs.bridge_id &&
39
+ self.id == rhs.id
40
+ end
41
+
42
+ private
43
+
44
+ def add_self_to_yaml(yaml)
45
+ key = self.name.dup.force_encoding('ASCII') # Avoid binary encoded YAML
46
+ bridge = bridge_id.dup.force_encoding('ASCII')
47
+ yaml[key] = {
48
+ STRING_ID => id.force_encoding('ASCII'),
49
+ STRING_BRIDGE_ID => bridge,
50
+ }
51
+ end
52
+
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,45 @@
1
+ module Hue
2
+ module Config
3
+ class Bridge < Abstract
4
+
5
+ def self.file_path
6
+ File.join(ENV['HOME'], ".#{Hue.device_type}", 'bridges.yml')
7
+ end
8
+
9
+ def self.find(id)
10
+ yaml = read_file(file_path)
11
+ entry = yaml.select { |k,v| k.to_s == id.to_s }
12
+ if entry.empty?
13
+ nil
14
+ else
15
+ new(id, entry[id]['uri'])
16
+ end
17
+ end
18
+
19
+ public
20
+
21
+ attr_accessor :uri
22
+
23
+ def initialize(id, uri, path = self.class.file_path)
24
+ super(id, path)
25
+ @uri = uri
26
+ end
27
+
28
+ def id
29
+ self.name
30
+ end
31
+
32
+ def write(overwrite_existing_key = true)
33
+ super(overwrite_existing_key)
34
+ end
35
+
36
+ private
37
+
38
+ def add_self_to_yaml(yaml)
39
+ key = id.dup.force_encoding('ASCII')
40
+ yaml[key] = {'uri' => self.uri.force_encoding('ASCII')}
41
+ end
42
+
43
+ end
44
+ end
45
+ end
data/lib/hue.rb CHANGED
@@ -1,11 +1,8 @@
1
- APP_NAME = 'ruby_hue'
2
- BASE = 'http://10.10.10.20/api'
3
- # UUID = 'd79713a3433df3d972ba7c22cb1cc23e'
4
- # Digest::MD5.hexdigest('aa4f6bc0-2045-0130-8cf0-0018de9ecdd0')
5
-
6
1
  require 'net/http'
7
2
  require 'json'
8
3
  require 'matrix'
4
+ require 'digest/md5'
5
+ require 'uuid'
9
6
 
10
7
  RGB_MATRIX = Matrix[
11
8
  [3.233358361244897, -1.5262682428425947, 0.27916711262124544],
@@ -13,20 +10,98 @@ RGB_MATRIX = Matrix[
13
10
  [0.12942207487871885, 0.19839858329512317, 2.0280912276039635]
14
11
  ]
15
12
 
16
- require 'hue/bridge.rb'
17
- require 'hue/bulb.rb'
18
- require 'hue/config.rb'
19
-
20
13
  module Hue
21
14
 
22
- DEVICE_TYPE = "RubyHue"
15
+ DEVICE_TYPE = 'hue-lib'
16
+ DEFAULT_UDP_TIMEOUT = 2
17
+ ERROR_DEFAULT_EXISTS = 'Default application already registered.'
18
+ ERROR_NO_BRIDGE_FOUND = 'No bridge found.'
23
19
 
24
20
  def self.device_type
25
21
  DEVICE_TYPE
26
22
  end
27
23
 
28
- def self.config
29
- Hue::Config.default
24
+ def self.one_time_uuid
25
+ Digest::MD5.hexdigest(UUID.generate)
26
+ end
27
+
28
+ def self.register_default
29
+ if default = (Hue::Config::Application.default rescue nil)
30
+ raise Hue::Error.new(ERROR_DEFAULT_EXISTS)
31
+ else
32
+ bridge_config = register_bridges.values.first # Assuming one bridge for now
33
+ secret = Hue.one_time_uuid
34
+ app_config = Hue::Config::Application.new(bridge_config.id, secret)
35
+ puts "Registering app...(#{secret})"
36
+ instance = Hue::Bridge.new(app_config.id, bridge_config.uri)
37
+ instance.register
38
+ app_config.write
39
+ instance
40
+ end
41
+ end
42
+
43
+ def self.application
44
+ application_config = Hue::Config::Application.default
45
+ bridge_config = Hue::Config::Bridge.find(application_config.bridge_id)
46
+ bridge_config ||= register_bridges[application_config.bridge_id]
47
+
48
+ if bridge_config.nil?
49
+ raise Error.new("Unable to find bridge: #{application_config.bridge_id}")
50
+ end
51
+
52
+ Hue::Bridge.new(application_config.id, bridge_config.uri)
53
+ end
54
+
55
+ def self.remove_default
56
+ instance = application
57
+ instance.unregister
58
+ Hue::Config::Application.default.delete
59
+ true
60
+ end
61
+
62
+ def self.discover(timeout = DEFAULT_UDP_TIMEOUT)
63
+ bridges = Hash.new
64
+ payload = <<-PAYLOAD
65
+ M-SEARCH * HTTP/1.1
66
+ HOST: 239.255.255.250:1900
67
+ MAN: ssdp:discover
68
+ MX: 10
69
+ ST: ssdp:all
70
+ PAYLOAD
71
+ broadcast_address = '239.255.255.250'
72
+ port_number = 1900
73
+
74
+ socket = UDPSocket.new(Socket::AF_INET)
75
+ socket.send(payload, 0, broadcast_address, port_number)
76
+
77
+ Timeout.timeout(timeout, Hue::Error) do
78
+ loop do
79
+ message, (address_family, port, hostname, ip_add) = socket.recvfrom(1024)
80
+ if message =~ /IpBridge/ && location = /LOCATION: (.*)$/.match(message)
81
+ if uuid = /uuid:(.{36})/.match(message)
82
+ # Assume this is Philips Hue for now.
83
+ bridges[uuid.captures.first] = "http://#{ip_add}/api"
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ rescue Hue::Error
90
+ bridges
91
+ end
92
+
93
+ def self.register_bridges
94
+ bridges = self.discover
95
+ if bridges.empty?
96
+ raise Error.new(ERROR_NO_BRIDGE_FOUND)
97
+ else
98
+ bridges.inject(Hash.new) do |hash, (id, ip)|
99
+ config = Hue::Config::Bridge.new(id, ip)
100
+ config.write
101
+ hash[id] = config
102
+ hash
103
+ end
104
+ end
30
105
  end
31
106
 
32
107
  class Error < StandardError
@@ -57,3 +132,9 @@ module Hue
57
132
  end
58
133
 
59
134
  end
135
+
136
+ require 'hue/config/abstract'
137
+ require 'hue/config/application'
138
+ require 'hue/config/bridge'
139
+ require 'hue/bridge'
140
+ require 'hue/bulb'
@@ -0,0 +1,4 @@
1
+ ---
2
+ default:
3
+ bridge_id: bc6be180-4c57-0130-8d8f-0018de9ecdd0
4
+ id: application_uuid
@@ -1,4 +1,3 @@
1
1
  ---
2
- default:
3
- base_uri: http://localhost/api
4
- identifier: test_identifier
2
+ bc6be180-4c57-0130-8d8f-0018de9ecdd0:
3
+ uri: http://localhost/api
@@ -2,36 +2,12 @@ require 'spec_helper.rb'
2
2
 
3
3
  describe Hue::Bridge do
4
4
 
5
- def self.klass
6
- Hue::Bridge
7
- end
8
-
9
- def klass
10
- self.class.klass
11
- end
12
-
13
- # it 'should acts as a singleton and give access to the instance' do
14
- # klass.instance.should be_a_kind_of(Hue::Bridge)
15
- # end
16
-
17
- it 'should allow registering a new bridge' do
18
- pending
19
- end
20
-
21
- it 'should allow un-registering a bridge' do
22
- pending
23
- end
5
+ context 'when instantiated' do
6
+ bridge = test_bridge
24
7
 
25
- context 'when instantiated with a given config' do
26
- bridge = klass.new
27
-
28
- # before(:each) do
29
- # with_fake_index_request
30
- # end
31
-
32
- it 'should report the bridge status' do
33
- with_fake_request_base
34
- bridge.status.should == api_reply(:base)
8
+ it 'should report the status' do
9
+ with_fake_request
10
+ bridge.status.should == api_reply(:get_success)
35
11
  end
36
12
 
37
13
  it 'should report errors' do
@@ -41,7 +17,7 @@ describe Hue::Bridge do
41
17
  end.should raise_error(Hue::API::Error, 'unauthorized user')
42
18
  end
43
19
 
44
- it 'should report the bridge lights' do
20
+ it 'should report the available lights' do
45
21
  with_fake_request(:lights)
46
22
  bridge.lights.should == api_reply(:lights)
47
23
  end
@@ -70,6 +46,20 @@ describe Hue::Bridge do
70
46
  end
71
47
  end
72
48
 
49
+ it 'should allow unregistering an existing config' do
50
+ with_fake_delete("config/whitelist/#{TEST_APPLICATION_UUID}")
51
+ bridge.unregister
52
+ end
53
+ end
54
+
55
+ context 'when instantiated with a new config' do
56
+ new_id = 'new_test_id'
57
+ bridge = described_class.new(new_id, TEST_BRIDGE_URI)
58
+
59
+ it 'should allow registering the new config' do
60
+ with_fake_post(nil, {:username => new_id, :devicetype => Hue.device_type})
61
+ bridge.register
62
+ end
73
63
  end
74
64
 
75
65
  end
@@ -2,16 +2,8 @@ require 'spec_helper.rb'
2
2
 
3
3
  describe Hue::Bulb do
4
4
 
5
- def self.klass
6
- Hue::Bulb
7
- end
8
-
9
- def klass
10
- self.class.klass
11
- end
12
-
13
5
  context 'when instantiated with a given bridge and id' do
14
- bulb = klass.new(Hue::Bridge.new, 1)
6
+ bulb = described_class.new(test_bridge, 1)
15
7
 
16
8
  before(:all) do
17
9
  with_fake_request('lights/1')
@@ -104,4 +96,5 @@ describe Hue::Bulb do
104
96
  end
105
97
 
106
98
  end
99
+
107
100
  end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper.rb'
2
+
3
+ class Hue::Config::AbstractTest < Hue::Config::Abstract
4
+ private
5
+ def add_self_to_yaml(yaml)
6
+ yaml[name] = { 1 => :test}
7
+ end
8
+ end
9
+
10
+ describe Hue::Config::Abstract do
11
+
12
+ before(:all) do
13
+ create_test_application_config
14
+ end
15
+
16
+ context 'given an new config' do
17
+ config = described_class.new('test', TEST_CONFIG_APPLICATION_PATH)
18
+
19
+ it 'should report the values' do
20
+ config.name == 'test'
21
+ config.path == TEST_CONFIG_APPLICATION_PATH
22
+ end
23
+
24
+ it 'should allow writing the new config to file' do
25
+ config.write
26
+ YAML.load_file(config.path)['test'].should be_a(Hash)
27
+ end
28
+
29
+ it 'should allow deleting that named config from the file' do
30
+ config.delete
31
+ YAML.load_file(config.path)['test'].should be_nil
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,61 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hue::Config::Application do
4
+
5
+ mock_application_config_path
6
+
7
+ after(:all) do
8
+ create_test_application_config
9
+ end
10
+
11
+ it 'should report the config file location' do
12
+ described_class.file_path.should == TEST_CONFIG_APPLICATION_PATH
13
+ end
14
+
15
+ it "should throw and error if a named config doesn't exist" do
16
+ lambda do
17
+ described_class.named('not_default')
18
+ end.should raise_error(Hue::Config::NotFound, /Config named (.*) not found/)
19
+ end
20
+
21
+ context 'with a config file, containing a default' do
22
+ config = described_class.default
23
+
24
+ it "should give the default config and report it's values" do
25
+ config.name == described_class::STRING_DEFAULT
26
+ config.bridge_id == TEST_CONFIG_APPLICATION[config.name][described_class::STRING_BRIDGE_ID]
27
+ config.id == TEST_CONFIG_APPLICATION[config.name][described_class::STRING_ID]
28
+ end
29
+
30
+ it 'should allow deleting the default config from the file' do
31
+ config.delete
32
+ YAML.load_file(described_class.file_path)[described_class::STRING_DEFAULT].should be_nil
33
+ end
34
+ end
35
+
36
+ context 'given an new config' do
37
+ config = described_class.new('http://someip/api', 'some_id', 'not_default')
38
+
39
+ it 'should report the values' do
40
+ config.name == 'not_default'
41
+ config.bridge_id == 'http://someip/api'
42
+ config.id == 'not_default'
43
+ end
44
+
45
+ it 'should allow writing the new config to file' do
46
+ config.write
47
+ YAML.load_file(described_class.file_path)['not_default'].should be_a(Hash)
48
+ end
49
+
50
+ it 'should allow fetching that name config' do
51
+ named_config = described_class.named('not_default')
52
+ named_config.should == config
53
+ end
54
+
55
+ it 'should allow deleting that named config from the file' do
56
+ config.delete
57
+ YAML.load_file(described_class.file_path)['not_default'].should be_nil
58
+ end
59
+ end
60
+
61
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper.rb'
2
+
3
+ describe Hue::Config::Bridge do
4
+
5
+ mock_bridge_config_path
6
+
7
+ after(:all) do
8
+ create_test_bridge_config
9
+ end
10
+
11
+ it 'should report the config file location' do
12
+ described_class.file_path.should == TEST_CONFIG_BRIDGE_PATH
13
+ end
14
+
15
+ it 'should find existing bridges given an id' do
16
+ found = described_class.find(TEST_BRIDGE_UUID)
17
+ found.id.should == TEST_BRIDGE_UUID
18
+ found.uri.should == TEST_BRIDGE_URI
19
+
20
+ described_class.find('something').should be_nil
21
+ end
22
+
23
+ context 'given an new config' do
24
+ uuid = UUID.generate
25
+ uri = 'http://someip/api'
26
+ config = described_class.new(uuid, uri)
27
+
28
+ it 'should report the values' do
29
+ config.name == uuid
30
+ config.id == uuid
31
+ config.uri == uri
32
+ end
33
+ end
34
+
35
+ end
data/spec/hue_spec.rb CHANGED
@@ -2,12 +2,79 @@ require 'spec_helper.rb'
2
2
 
3
3
  describe Hue do
4
4
 
5
+ mock_application_config_path
6
+ mock_bridge_config_path
7
+
8
+ after(:each) do
9
+ create_test_bridge_config
10
+ end
11
+
12
+ before(:each) do
13
+ mock_udp_replies
14
+ end
15
+
5
16
  it 'should report the device type as itself' do
6
- Hue.device_type.should == Hue::DEVICE_TYPE
17
+ described_class.device_type.should == described_class::DEVICE_TYPE
7
18
  end
8
19
 
9
- it 'should return the default config' do
10
- Hue.config.should be_a(Hue::Config)
20
+ it 'should return the default application' do
21
+ described_class.application.should be_a(described_class::Bridge)
22
+ end
23
+
24
+ context 'when discovering new bridges' do
25
+ it 'should return a list discovered bridges' do
26
+ bridges = Hue.discover
27
+ bridges.should == {TEST_UDP_BRIDGE_UUID => TEST_UDP_BRIDGE_URI}
28
+ end
29
+
30
+ it 'should allow registering of discovered bridges' do
31
+ Hue::Config::Bridge.find(TEST_UDP_BRIDGE_UUID).should be_nil
32
+ registered = Hue.register_bridges
33
+ new_bridge = registered[TEST_UDP_BRIDGE_UUID]
34
+ new_bridge.id.should == TEST_UDP_BRIDGE_UUID
35
+ new_bridge.uri.should == TEST_UDP_BRIDGE_URI
36
+ end
37
+ end
38
+
39
+ context 'after discovering bridges' do
40
+ before(:each) do
41
+ mock_udp_replies(TEST_BRIDGE_UUID, 'new_host')
42
+ end
43
+
44
+ it 'should update already registered bridges' do
45
+ bridge = Hue::Config::Bridge.find(TEST_BRIDGE_UUID)
46
+ bridge.should_not be_nil
47
+
48
+ registered = Hue.register_bridges
49
+
50
+ updated_bridge = registered[TEST_BRIDGE_UUID]
51
+ updated_bridge.id.should == TEST_BRIDGE_UUID
52
+ updated_bridge.uri.should_not == bridge.uri
53
+ end
54
+ end
55
+
56
+ context 'when registering or un-registering the application' do
57
+ it 'should throw and error if the default already exists' do
58
+ lambda do
59
+ described_class.register_default
60
+ end.should raise_error(described_class::Error, described_class::ERROR_DEFAULT_EXISTS)
61
+ end
62
+
63
+ it 'should allow a new default if one doesn\'t exist' do
64
+ with_temp_config_path do
65
+ with_fake_post(nil, {}, 'post_success', TEST_UDP_BRIDGE_URI)
66
+ with_stdout(/Registering app...(.*)$/) do
67
+ instance = described_class.register_default
68
+ end
69
+ end
70
+ end
71
+
72
+ it 'should allow un-registering the default' do
73
+ with_temp_config_path(true) do
74
+ with_fake_delete("config/whitelist/#{TEST_APPLICATION_UUID}")
75
+ instance = described_class.remove_default
76
+ end
77
+ end
11
78
  end
12
79
 
13
80
  end
File without changes
@@ -0,0 +1 @@
1
+ {"error"=>{"type"=>101, "address"=>"", "description"=>"link button not pressed"}}
@@ -0,0 +1,8 @@
1
+ [
2
+ {
3
+ "success":
4
+ {
5
+ "username":"new_test_id"
6
+ }
7
+ }
8
+ ]
@@ -0,0 +1 @@
1
+ [{}]
data/spec/spec_helper.rb CHANGED
@@ -5,21 +5,113 @@ require 'mocha'
5
5
 
6
6
  WebMock.disable_net_connect!
7
7
 
8
+ # GENERAL
9
+
10
+ def join_paths(*paths)
11
+ File.join(paths.delete_if { |entry| entry.nil? || entry.empty? })
12
+ end
13
+
14
+ def with_stdout(expected_output, &block)
15
+ original_stdout = $stdout
16
+ new_stdout = StringIO.new
17
+ begin
18
+ $stdout = new_stdout
19
+ yield
20
+ new_stdout.seek(0)
21
+ output = new_stdout.read
22
+ output.should match(expected_output)
23
+ ensure
24
+ new_stdout.close
25
+ $stdout = original_stdout
26
+ end
27
+ end
28
+
29
+ def silence_warnings
30
+ begin
31
+ old_verbose, $VERBOSE = $VERBOSE, nil
32
+ yield
33
+ ensure
34
+ $VERBOSE = old_verbose
35
+ end
36
+ end
37
+
8
38
  SPEC_DIR = File.dirname(__FILE__)
9
- TEST_BRIDGE_CONFIG_PATH = File.join(SPEC_DIR, 'config', 'bridges.yml')
10
- TEST_BRIDGE_CONFIG = YAML.load_file(TEST_BRIDGE_CONFIG_PATH)
39
+
40
+ silence_warnings do
41
+ Hue.const_set(:DEFAULT_UDP_TIMEOUT, 0.01)
42
+ end
43
+
44
+ # APPLICATION CONFIG
45
+
46
+ TEST_CONFIG_APPLICATION_PATH = File.join(SPEC_DIR, 'config', 'applications.yml')
47
+ TEST_CONFIG_APPLICATION = YAML.load_file(TEST_CONFIG_APPLICATION_PATH)
11
48
  TEST_JSON_DATA_PATH = File.join(SPEC_DIR, 'json')
12
49
 
13
- Hue::Config.expects(:bridges_config_path).at_least_once.returns(TEST_BRIDGE_CONFIG_PATH)
50
+ def create_test_application_config(path = TEST_CONFIG_APPLICATION_PATH)
51
+ File.open(path, 'w' ) do |out|
52
+ YAML.dump(TEST_CONFIG_APPLICATION, out)
53
+ end
54
+ end
14
55
 
15
- def with_fake_request_base
16
- stub_request(:get, "http://localhost/api/test_identifier").
17
- to_return(:status => 200, :body => api_reply_json(:base), :headers => {})
56
+ def mock_application_config_path
57
+ Hue::Config::Application.stubs(:file_path).returns(TEST_CONFIG_APPLICATION_PATH)
18
58
  end
19
59
 
20
- def with_fake_request(named, body_name = named)
21
- stub_request(:get, "http://localhost/api/test_identifier/#{named.to_s}").
22
- to_return(:status => 200, :body => api_reply_json(body_name), :headers => {})
60
+ def with_temp_config_path(write_config = false)
61
+ temp_config_path = File.join(SPEC_DIR, 'config', 'temp')
62
+ FileUtils.mkdir_p(temp_config_path)
63
+ temp_config = File.join(temp_config_path, 'applications.yml')
64
+ if write_config
65
+ create_test_application_config(temp_config)
66
+ end
67
+ Hue::Config::Application.expects(:file_path).at_least_once.returns(temp_config)
68
+
69
+ begin
70
+ yield
71
+ ensure
72
+ FileUtils.rm_f(temp_config)
73
+ mock_bridge_config_path
74
+ end
75
+ end
76
+
77
+ # BRIDGE CONFIG
78
+
79
+ TEST_CONFIG_BRIDGE_PATH = File.join(SPEC_DIR, 'config', 'bridges.yml')
80
+ TEST_BRIDGE_UUID = 'bc6be180-4c57-0130-8d8f-0018de9ecdd0'
81
+ TEST_CONFIG_BRIDGE = YAML.load_file(TEST_CONFIG_BRIDGE_PATH)
82
+
83
+ def create_test_bridge_config
84
+ File.open(TEST_CONFIG_BRIDGE_PATH, 'w' ) do |out|
85
+ YAML.dump(TEST_CONFIG_BRIDGE, out)
86
+ end
87
+ end
88
+
89
+ def mock_bridge_config_path
90
+ Hue::Config::Bridge.stubs(:file_path).returns(TEST_CONFIG_BRIDGE_PATH)
91
+ end
92
+
93
+ # HUE - UDP
94
+
95
+ TEST_UDP_BRIDGE_UUID = '09230030-4c1e-0130-8d83-0018de9ecdd0'
96
+ TEST_UDP_BRIDGE_HOSTNAME = 'upd-host'
97
+ TEST_UDP_BRIDGE_URI = "http://#{TEST_UDP_BRIDGE_HOSTNAME}/api"
98
+
99
+ def mock_udp_replies(uuid = TEST_UDP_BRIDGE_UUID, hostname = TEST_UDP_BRIDGE_HOSTNAME)
100
+ reply = ["HTTP/1.1 200 OK\r\nCACHE-CONTROL: max-age=100\r\nEXT:\r\nLOCATION: http://127.0.0.1:80/description.xml\r\nSERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/0.1\r\nST: upnp:rootdevice\r\nUSN: uuid:#{uuid}::upnp:rootdevice\r\n\r\n", ["AF_INET", 1900, "127.0.0.1", hostname]]
101
+
102
+ socket = Object.new
103
+ socket.stubs(:send).returns(nil)
104
+ socket.stubs(:recvfrom).returns(reply)
105
+ UDPSocket.stubs(:new).returns(socket)
106
+ end
107
+
108
+ # BRIDGE - API CALLS
109
+
110
+ TEST_BRIDGE_URI = 'http://localhost/api'
111
+ TEST_APPLICATION_UUID = 'application_uuid'
112
+
113
+ def test_bridge
114
+ Hue::Bridge.new(TEST_APPLICATION_UUID, TEST_BRIDGE_URI)
23
115
  end
24
116
 
25
117
  def api_reply_json(named)
@@ -32,10 +124,37 @@ def api_reply(named)
32
124
  JSON.parse(api_reply_json(named))
33
125
  end
34
126
 
35
- def with_fake_update(named, update = {})
36
- stub = stub_request(:put, "http://localhost/api/test_identifier/#{named.to_s}").
37
- with(:body => update.to_json).
38
- to_return(:status => 200, :body => update.to_json, :headers => {})
127
+ def with_fake_request(named = nil, body_name = nil)
128
+ body_name ||= (named.nil? ? 'get_success' : named)
129
+ stub_request(:get, join_paths(TEST_BRIDGE_URI, TEST_APPLICATION_UUID, named.to_s)).
130
+ to_return(:status => 200, :body => api_reply_json(body_name), :headers => {})
131
+ end
132
+
133
+ def with_fake_update(named, put_body = {})
134
+ stub = stub_request(:put, "#{TEST_BRIDGE_URI}/#{TEST_APPLICATION_UUID}/#{named.to_s}").
135
+ with(:body => put_body.to_json).
136
+ to_return(:status => 200, :body => api_reply_json(:put_success), :headers => {})
137
+
138
+ if block_given?
139
+ yield
140
+ stub.should have_been_requested
141
+ end
142
+ end
143
+
144
+ def with_fake_post(named, post_body = {}, post_reply_name = 'post_success', uri = TEST_BRIDGE_URI)
145
+ stub = stub_request(:post, join_paths(uri, named))
146
+ stub.with(:body => post_body.to_json) unless post_body.empty?
147
+ stub.to_return(:status => 200, :body => api_reply_json(join_paths(named, post_reply_name)), :headers => {})
148
+
149
+ if block_given?
150
+ yield
151
+ stub.should have_been_requested
152
+ end
153
+ end
154
+
155
+ def with_fake_delete(named, delete_reply = 'delete_success')
156
+ stub = stub_request(:delete, "#{TEST_BRIDGE_URI}/#{TEST_APPLICATION_UUID}/#{named.to_s}").
157
+ to_return(:status => 200, :body => api_reply_json(join_paths(named, delete_reply)), :headers => {})
39
158
 
40
159
  if block_given?
41
160
  yield
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hue-lib
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
8
8
  - Birkir A. Barkarson
9
- - ''
9
+ - Aaron Hurley
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2013-01-10 00:00:00.000000000 Z
13
+ date: 2013-01-30 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: json
@@ -83,6 +83,7 @@ executables: []
83
83
  extensions: []
84
84
  extra_rdoc_files: []
85
85
  files:
86
+ - .gitignore
86
87
  - Gemfile
87
88
  - README.md
88
89
  - Rakefile
@@ -90,16 +91,25 @@ files:
90
91
  - lib/hue.rb
91
92
  - lib/hue/bridge.rb
92
93
  - lib/hue/bulb.rb
93
- - lib/hue/config.rb
94
+ - lib/hue/config/abstract.rb
95
+ - lib/hue/config/application.rb
96
+ - lib/hue/config/bridge.rb
97
+ - spec/config/applications.yml
94
98
  - spec/config/bridges.yml
95
99
  - spec/hue/bridge_spec.rb
96
100
  - spec/hue/bulb_spec.rb
97
- - spec/hue/config_spec.rb
101
+ - spec/hue/config/abstract_spec.rb
102
+ - spec/hue/config/application_spec.rb
103
+ - spec/hue/config/bridge_spec.rb
98
104
  - spec/hue_spec.rb
99
- - spec/json/base.json
100
105
  - spec/json/config.json
106
+ - spec/json/config/whitelist/application_uuid/delete_success.json
107
+ - spec/json/get_success.json
101
108
  - spec/json/lights.json
102
109
  - spec/json/lights/1.json
110
+ - spec/json/post_error.json
111
+ - spec/json/post_success.json
112
+ - spec/json/put_success.json
103
113
  - spec/json/schedules.json
104
114
  - spec/json/unauthorized.json
105
115
  - spec/spec_helper.rb
data/lib/hue/config.rb DELETED
@@ -1,94 +0,0 @@
1
- module Hue
2
- class Config
3
- STRING_DEFAULT = 'default'
4
- STRING_BASE_URI = 'base_uri'
5
- STRING_IDENTIFIER = 'identifier'
6
-
7
- require 'yaml'
8
- require 'fileutils'
9
-
10
- def self.bridges_config_path
11
- File.join(ENV['HOME'], ".#{APP_NAME}", 'bridges.yml')
12
- end
13
-
14
- def self.default
15
- named(STRING_DEFAULT)
16
- end
17
-
18
- def self.named(name)
19
- yaml = read_file
20
- if named_yaml = yaml[name]
21
- Config.new(named_yaml[STRING_BASE_URI], named_yaml[STRING_IDENTIFIER], name)
22
- else
23
- raise Error.new("Config named '#{name}' not found.")
24
- end
25
- end
26
-
27
- public
28
-
29
- attr_reader :base_uri, :identifier, :name
30
-
31
- def initialize(base_uri, identifier, name = STRING_DEFAULT)
32
- @base_uri = base_uri
33
- @identifier = identifier
34
- @name = name
35
- end
36
-
37
- def write(config_file = self.class.bridges_config_path)
38
- yaml = YAML.load_file(self.class.bridges_config_path) rescue Hash::New
39
- if yaml.key?(name)
40
- raise "Configuration named '#{name}' already exists in #{config_file}\nPlease de-register before creating a new one with the same name."
41
- else
42
- yaml[name] = {
43
- STRING_BASE_URI => self.base_uri,
44
- STRING_IDENTIFIER => identifier.force_encoding('ASCII') # Avoid binary encoded YAML
45
- }
46
- self.class.setup_config_path(config_file)
47
- File.open(config_file, 'w+' ) do |out|
48
- YAML.dump(yaml, out)
49
- end
50
- end
51
- end
52
-
53
- def delete
54
- config_file = self.class.bridges_config_path
55
- yaml = YAML.load_file(config_file) rescue Hash::New
56
-
57
- if yaml.key?(name)
58
- yaml.delete(name)
59
- end
60
-
61
- if yaml.size > 0
62
- self.class.setup_config_path(config_file)
63
- File.open(config_file, 'w+' ) do |out|
64
- YAML.dump(yaml, out)
65
- end
66
- end
67
- end
68
-
69
- def ==(rhs)
70
- lhs = self
71
-
72
- lhs.class == rhs.class &&
73
- lhs.name == rhs.name &&
74
- lhs.base_uri == rhs.base_uri &&
75
- lhs.identifier == rhs.identifier
76
- end
77
-
78
- private
79
-
80
- def self.setup_config_path(path)
81
- dir = File.dirname(path)
82
- FileUtils.mkdir_p(dir) unless Dir.exists?(dir)
83
- end
84
-
85
- def self.read_file(config_file = bridges_config_path)
86
- begin
87
- yaml = YAML.load_file(config_file)
88
- rescue => err
89
- raise Error.new("Failed to read configuration file", err)
90
- end
91
- end
92
-
93
- end
94
- end
@@ -1,65 +0,0 @@
1
- require 'spec_helper.rb'
2
-
3
- describe Hue::Config do
4
-
5
- TEST_IDENTIFIER = 'test_identifier'
6
-
7
- def self.klass
8
- Hue::Config
9
- end
10
-
11
- def klass
12
- self.class.klass
13
- end
14
-
15
- after(:all) do
16
- File.open(TEST_BRIDGE_CONFIG_PATH, 'w' ) do |out|
17
- YAML.dump(TEST_BRIDGE_CONFIG, out)
18
- end
19
- end
20
-
21
- it 'should report the bridge config file location' do
22
- klass.bridges_config_path.should == TEST_BRIDGE_CONFIG_PATH
23
- end
24
-
25
- it "should throw and error if a named config doesn't exist" do
26
- lambda do
27
- klass.named('not_default')
28
- end.should raise_error(Hue::Error, /Config named (.*) not found/)
29
- end
30
-
31
- context 'with a bridge config file, containing the default bridge' do
32
- it "should give the default config and report it's values" do
33
- config = klass.default
34
- config.name == klass::STRING_DEFAULT
35
- config.base_uri == TEST_BRIDGE_CONFIG[config.name][klass::STRING_BASE_URI]
36
- config.identifier == TEST_BRIDGE_CONFIG[config.name][klass::STRING_IDENTIFIER]
37
- end
38
- end
39
-
40
- context 'given an new config' do
41
- config = klass.new('http://someip/api', 'some_id', 'not_default')
42
-
43
- it 'should report the values' do
44
- config.name == 'not_default'
45
- config.base_uri == 'http://someip/api'
46
- config.identifier == 'not_default'
47
- end
48
-
49
- it 'should allow writing the new config to file' do
50
- config.write
51
- YAML.load_file(klass.bridges_config_path)['not_default'].should be_a(Hash)
52
- end
53
-
54
- it 'should allow fetching that name config' do
55
- named_config = klass.named('not_default')
56
- named_config.should == config
57
- end
58
-
59
- it 'should allow deleting that named config from the file' do
60
- config.delete
61
- YAML.load_file(klass.bridges_config_path)['not_default'].should be_nil
62
- end
63
- end
64
-
65
- end