huemote 0.0.2
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 +7 -0
- data/.gitignore +20 -0
- data/.travis.yml +8 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +20 -0
- data/README.md +59 -0
- data/Rakefile +8 -0
- data/huemote.gemspec +21 -0
- data/lib/huemote.rb +15 -0
- data/lib/huemote/bridge.rb +136 -0
- data/lib/huemote/client.rb +83 -0
- data/lib/huemote/light.rb +76 -0
- data/lib/huemote/version.rb +3 -0
- data/spec/huemote/bridge_spec.rb +12 -0
- data/spec/spec_helper.rb +1 -0
- metadata +61 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9be51c602e6af5ead50c6971f39506a6a5032421
|
4
|
+
data.tar.gz: 114b210e64560c8a666030a6098e37767da0bc2e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4b2a0ff35970f5de67a3dc2997370f0cc758200ea12e01224abd329872365555b17ed6350e60af22481d00416efaaa82f0fb6c2097ece58f3c76da56154cfd5a
|
7
|
+
data.tar.gz: 63f14a307fce18f9cbeaeb9552de0b8612a57547c576ee702f8025a571f6803f192521786af8cb647652b588530782201ea2d2b7c6ead929afe559e44f24bd39
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2014 Kevin Gisi
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
# Huemote [](https://travis-ci.org/gisikw/huemote) [](http://badge.fury.io/rb/huemote) [](https://codeclimate.com/github/gisikw/huemote)
|
2
|
+
|
3
|
+
Huemote is an interface for controlling Philips Hue lights. Unlike other implementations, it does not rely on Philips backend servers for upnp discovery.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'huemote'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install huemote
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
You can fetch all lights on your network via:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
lights = Huemote::Light.all #=> [#<Huemote::Light:0x511ee8dd @name="Hallway", @id="1">, #<Huemote::Light:0x444a2ec6 @name="Main Room", @id="2">, #<Huemote::Light:0x6244ec30 @name="Bathroom 1", @id="3">, #<Huemote::Light:0x1aee75b7 @name="Bathroom 2", @id="4">, #<Huemote::Light:0x1d724f31 @name="Bathroom 3", @id="5">]
|
25
|
+
```
|
26
|
+
|
27
|
+
Or select a light by its friendly name:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
switch = Huemote::Light.find('Hallway') #=> #<Huemote::Light:0x511ee8dd @name="Hallway", @id="1">
|
31
|
+
```
|
32
|
+
|
33
|
+
Given a Light instance, you can call the following methods:
|
34
|
+
```ruby
|
35
|
+
light.off? #=> [true,false]
|
36
|
+
light.on? #=> [true,false]
|
37
|
+
light.on!
|
38
|
+
light.off!
|
39
|
+
light.toggle!
|
40
|
+
light.brightness(250)
|
41
|
+
light.saturation(50)
|
42
|
+
light.effect('colorloop')
|
43
|
+
light.alert('select')
|
44
|
+
light.hue(5)
|
45
|
+
light.xy([0.123,0.425])
|
46
|
+
light.ct(300)
|
47
|
+
```
|
48
|
+
|
49
|
+
## Performance
|
50
|
+
|
51
|
+
Huemote is designed to be performant - and as such, it will leverage the best HTTP library available for making requests. Currently, Wemote will use (in order of preference): `manticore`, `typhoeus`, `httparty`, and finally (miserably) `net/http`. Because you probably like things fast too, we recommend you `gem install manticore` on JRuby, or `gem install typhoeus` on another engine. In order to keep the gem as flexible as possible, none of these are direct dependencies. They just make Wemote happy and fast.
|
52
|
+
|
53
|
+
## Contributing
|
54
|
+
|
55
|
+
1. Fork it
|
56
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
57
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
58
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
59
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/huemote.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'huemote/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "huemote"
|
8
|
+
spec.version = Huemote::VERSION
|
9
|
+
spec.authors = ["Kevin Gisi"]
|
10
|
+
spec.email = ["kevin@kevingisi.com"]
|
11
|
+
spec.description = %q{Huemote is a Ruby gem for managing Philips Hue lights}
|
12
|
+
spec.summary = %q{Huemote is a Ruby gem for managing Philips Hue lights}
|
13
|
+
spec.homepage = "https://github.com/gisikw/huemote"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.platform = 'ruby'
|
16
|
+
|
17
|
+
spec.files = `git ls-files`.split($/)
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
end
|
data/lib/huemote.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
require_relative './huemote/version'
|
4
|
+
|
5
|
+
module Huemote
|
6
|
+
require_relative './huemote/client'
|
7
|
+
require_relative './huemote/light'
|
8
|
+
require_relative './huemote/bridge'
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def discover(socket=nil)
|
12
|
+
Huemote::Bridge.discover
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'ipaddr'
|
3
|
+
|
4
|
+
module Huemote
|
5
|
+
class Bridge
|
6
|
+
DEVICE_TYPE = "Huemote"
|
7
|
+
USERNAME = "HuemoteRubyGem"
|
8
|
+
MULTICAST_ADDR = '239.255.255.250'
|
9
|
+
BIND_ADDR = '0.0.0.0'
|
10
|
+
PORT = 1900
|
11
|
+
DISCOVERY= <<-EOF
|
12
|
+
M-SEARCH * HTTP/1.1\r
|
13
|
+
HOST: 239.255.255.250:1900\r
|
14
|
+
MAN: "ssdp:discover"\r
|
15
|
+
MX: 10\r
|
16
|
+
ST: urn:schemas-upnp-org:device:Basic:1\r
|
17
|
+
\r
|
18
|
+
EOF
|
19
|
+
|
20
|
+
class << self
|
21
|
+
def get
|
22
|
+
@bridge ||= begin
|
23
|
+
client = Huemote::Client.new
|
24
|
+
body = nil
|
25
|
+
device = fetch_upnp.detect{|host,port|body = client.get("http://#{host}:#{port}/description.xml").body; body.match('<modelURL>http://www.meethue.com</modelURL>')}
|
26
|
+
self.new(*device,body)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def discover(socket = nil)
|
33
|
+
@bridge = nil
|
34
|
+
client = Huemote::Client.new
|
35
|
+
body = nil
|
36
|
+
|
37
|
+
devices, socket = fetch_upnp(true,socket)
|
38
|
+
device = devices.detect{|host,port|body = client.get("http://#{host}:#{port}/description.xml").body; body.match('<modelURL>http://www.meethue.com</modelURL>')}
|
39
|
+
@bridge = self.new(*device,body)
|
40
|
+
|
41
|
+
socket
|
42
|
+
end
|
43
|
+
|
44
|
+
def ssdp_socket
|
45
|
+
socket = UDPSocket.new
|
46
|
+
socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, IPAddr.new(MULTICAST_ADDR).hton + IPAddr.new(BIND_ADDR).hton)
|
47
|
+
socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, 1)
|
48
|
+
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEPORT, 1)
|
49
|
+
socket.bind(BIND_ADDR,PORT)
|
50
|
+
socket
|
51
|
+
end
|
52
|
+
|
53
|
+
def listen(socket,devices)
|
54
|
+
sleep 1
|
55
|
+
Thread.start do
|
56
|
+
loop do
|
57
|
+
message, _ = socket.recvfrom(1024)
|
58
|
+
match = message.match(/LOCATION:\s+http:\/\/([^\/]+)/)
|
59
|
+
devices << match[1].split(':') if match
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def fetch_upnp(return_socket=false,socket=nil)
|
65
|
+
socket ||= ssdp_socket
|
66
|
+
devices = []
|
67
|
+
|
68
|
+
3.times { socket.send(DISCOVERY, 0, MULTICAST_ADDR, PORT) }
|
69
|
+
|
70
|
+
# The following is a bit silly, but is necessary for JRuby support,
|
71
|
+
# which seems to have some issues with socket interruption. If you have
|
72
|
+
# a working JRuby solution that doesn't require this kind of hackery,
|
73
|
+
# by all means, submit a pull request!
|
74
|
+
|
75
|
+
listen(socket,devices).tap{|l|sleep 1; l.kill}
|
76
|
+
devices.uniq!
|
77
|
+
|
78
|
+
if return_socket
|
79
|
+
[devices,socket]
|
80
|
+
else
|
81
|
+
socket.close
|
82
|
+
devices
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
attr_accessor :name
|
88
|
+
|
89
|
+
def initialize(host,port,body)
|
90
|
+
@host, @port = host, port
|
91
|
+
@name = body.match(/<friendlyName>([^<]+)<\/friendlyName>/)[1]
|
92
|
+
end
|
93
|
+
|
94
|
+
def _get(path,params={})
|
95
|
+
request(:get,path,params)
|
96
|
+
end
|
97
|
+
|
98
|
+
def _post(path,params={})
|
99
|
+
request(:post,path,params)
|
100
|
+
end
|
101
|
+
|
102
|
+
def _put(path,params={})
|
103
|
+
request(:put,path,params)
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def request(method,path,params)
|
109
|
+
auth! unless authed?
|
110
|
+
JSON.parse(client.send(method,"#{base_url}/api/#{USERNAME}/#{path}",params.to_json).body)
|
111
|
+
end
|
112
|
+
|
113
|
+
def base_url
|
114
|
+
@base_url ||= "http://#{@host}:#{@port}"
|
115
|
+
end
|
116
|
+
|
117
|
+
def authed?
|
118
|
+
@authed ||= !client.get("#{base_url}/api/#{USERNAME}/lights").body.match('unauthorized user')
|
119
|
+
end
|
120
|
+
|
121
|
+
def auth!
|
122
|
+
unless authed?
|
123
|
+
body = client.post("#{base_url}/api",{devicetype:DEVICE_TYPE,username:USERNAME}.to_json).body
|
124
|
+
if body.match('link button not pressed')
|
125
|
+
puts "In order for the Hue Bridge to interact, please press the button just this once and try again."
|
126
|
+
false
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def client
|
132
|
+
@client ||= Huemote::Client.new
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Huemote
|
2
|
+
class Client
|
3
|
+
|
4
|
+
def self.technique
|
5
|
+
@technique ||= begin
|
6
|
+
constants.collect {|const_name| const_get(const_name)}.select {|const| const.class == Module}.detect do |mod|
|
7
|
+
fulfilled = false
|
8
|
+
begin
|
9
|
+
next unless mod.const_defined?(:DEPENDENCIES)
|
10
|
+
mod.const_get(:DEPENDENCIES).map{|d|require d}
|
11
|
+
fulfilled = true
|
12
|
+
rescue LoadError
|
13
|
+
end
|
14
|
+
fulfilled
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module SmartLib
|
20
|
+
%w{get post put}.each do |name|
|
21
|
+
define_method name do |*args|
|
22
|
+
_req(@lib,name,*args).tap{|r|r.call if @call}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module Manticore
|
28
|
+
DEPENDENCIES = ['manticore']
|
29
|
+
def self.extended(base)
|
30
|
+
base.instance_variable_set(:@lib,::Manticore)
|
31
|
+
base.instance_variable_set(:@call,true)
|
32
|
+
base.extend(SmartLib)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
module Typhoeus
|
37
|
+
DEPENDENCIES = ['typhoeus']
|
38
|
+
def self.extended(base)
|
39
|
+
base.instance_variable_set(:@lib,::Typhoeus)
|
40
|
+
base.extend(SmartLib)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module HTTParty
|
45
|
+
DEPENDENCIES = ['httparty']
|
46
|
+
def self.extended(base)
|
47
|
+
base.instance_variable_set(:@lib,::HTTParty)
|
48
|
+
base.extend(SmartLib)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
module NetHTTP
|
53
|
+
DEPENDENCIES = ['net/http','uri']
|
54
|
+
|
55
|
+
def request(klass,url,body=nil,headers=nil)
|
56
|
+
uri = URI.parse(url)
|
57
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
58
|
+
request = klass.new(uri.request_uri)
|
59
|
+
headers.map{|k,v|request[k]=v} if headers
|
60
|
+
(request.body = body) if body
|
61
|
+
response = http.request(request)
|
62
|
+
end
|
63
|
+
|
64
|
+
%w{get post put}.each do |name|
|
65
|
+
define_method name do |*args|
|
66
|
+
request(Net::HTTP.const_get(name.capitalize),*args)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
def initialize
|
73
|
+
extend Huemote::Client.technique
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def _req(lib,method,url,body=nil,headers=nil)
|
79
|
+
lib.send(method,url,{body:body,headers:headers})
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Huemote
|
2
|
+
class Light
|
3
|
+
|
4
|
+
STATES = {
|
5
|
+
brightness: 'bri',
|
6
|
+
saturation: 'sat',
|
7
|
+
effect: 'effect',
|
8
|
+
alert: 'alert',
|
9
|
+
hue: 'hue',
|
10
|
+
xy: 'xy',
|
11
|
+
ct: 'ct'
|
12
|
+
}
|
13
|
+
|
14
|
+
class << self
|
15
|
+
|
16
|
+
def all(refresh=false)
|
17
|
+
@lights = nil if refresh
|
18
|
+
@lights ||= bridge._get('lights').map{|id,h|self.new(id,h['name'])}
|
19
|
+
end
|
20
|
+
|
21
|
+
def find(name)
|
22
|
+
all.detect{|l|l.name == name}
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def bridge
|
28
|
+
@bridge ||= Huemote::Bridge.get
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_accessor :name
|
34
|
+
|
35
|
+
def initialize(id,name)
|
36
|
+
@id, @name = id, name
|
37
|
+
end
|
38
|
+
|
39
|
+
def on!
|
40
|
+
set!(on:true)
|
41
|
+
end
|
42
|
+
|
43
|
+
def off!
|
44
|
+
set!(on:false)
|
45
|
+
end
|
46
|
+
|
47
|
+
def on?
|
48
|
+
bridge._get("lights/#{@id}")['state']['on']
|
49
|
+
end
|
50
|
+
|
51
|
+
def off?
|
52
|
+
!on?
|
53
|
+
end
|
54
|
+
|
55
|
+
def toggle!
|
56
|
+
on? ? off! : on!
|
57
|
+
end
|
58
|
+
|
59
|
+
STATES.each do |name,state|
|
60
|
+
define_method name do |arg|
|
61
|
+
set!(state => arg)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def set!(params)
|
68
|
+
bridge._put("lights/#{@id}/state",params)
|
69
|
+
end
|
70
|
+
|
71
|
+
def bridge
|
72
|
+
@bridge ||= Huemote::Bridge.get
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Huemote::Bridge do
|
4
|
+
describe 'initialize' do
|
5
|
+
it 'should set the host, port and friendly name' do
|
6
|
+
bridge = Huemote::Bridge.new('fakehost',1234,'<friendlyName>Bob</friendlyName>')
|
7
|
+
bridge.instance_variable_get(:@host).should == 'fakehost'
|
8
|
+
bridge.instance_variable_get(:@port).should == 1234
|
9
|
+
bridge.instance_variable_get(:@name).should == 'Bob'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'huemote'
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: huemote
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kevin Gisi
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-03-07 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Huemote is a Ruby gem for managing Philips Hue lights
|
14
|
+
email:
|
15
|
+
- kevin@kevingisi.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- .gitignore
|
21
|
+
- .travis.yml
|
22
|
+
- Gemfile
|
23
|
+
- LICENSE.txt
|
24
|
+
- README.md
|
25
|
+
- Rakefile
|
26
|
+
- huemote.gemspec
|
27
|
+
- lib/huemote.rb
|
28
|
+
- lib/huemote/bridge.rb
|
29
|
+
- lib/huemote/client.rb
|
30
|
+
- lib/huemote/light.rb
|
31
|
+
- lib/huemote/version.rb
|
32
|
+
- spec/huemote/bridge_spec.rb
|
33
|
+
- spec/spec_helper.rb
|
34
|
+
homepage: https://github.com/gisikw/huemote
|
35
|
+
licenses:
|
36
|
+
- MIT
|
37
|
+
metadata: {}
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - '>='
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '0'
|
52
|
+
requirements: []
|
53
|
+
rubyforge_project:
|
54
|
+
rubygems_version: 2.2.2
|
55
|
+
signing_key:
|
56
|
+
specification_version: 4
|
57
|
+
summary: Huemote is a Ruby gem for managing Philips Hue lights
|
58
|
+
test_files:
|
59
|
+
- spec/huemote/bridge_spec.rb
|
60
|
+
- spec/spec_helper.rb
|
61
|
+
has_rdoc:
|