wemote 0.2.0 → 0.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b18903132febc3de01e2796ce0b7adde752b030f
4
- data.tar.gz: 07503eba981f501449f2b6f86afb90baca0ad7c7
3
+ metadata.gz: 88d7829f2bfa8a7892103c87c14696a2efcafcf0
4
+ data.tar.gz: 8b7e761fbfde70ab48e0ca55cdd50c388c51a9c6
5
5
  SHA512:
6
- metadata.gz: 3313aa5e43f3394d505f9a2b6c7dbfe06dc6f49effaaea8b86e43542e93f5cb4583793631b34cd6fac99be1aa3440e4ca3b9b40320a8d4ba36fea52418e1662a
7
- data.tar.gz: ad62e4b4497c8509246861e52479b677ce495d1859103fbd67823552af6b92f346ad7972cd1dbc8f54e1b7129aa52429172691be75eb2331e639bd09e879b237
6
+ metadata.gz: f7c758531b285a15f3fa50b0eadf8aae1c56ae746b73909789efa795d08414ca1b5f47f8af8fff56aefb48ac9ccd99c0a8d1844dedc4eb1b53a3e9569f84af12
7
+ data.tar.gz: a9ca631f0509ef3a4d8cab76a1e229b1507a003ccfb62f0e8261ea6669be7ff4799e89944732bc1698394a19b7e6f0e9c1d3f70ac8c9dbb04eb95505472acb06
data/Gemfile CHANGED
@@ -4,7 +4,6 @@ gemspec
4
4
  # Commented out until I can determine a way to set engine-specific dependencies
5
5
  # gem 'manticore', git: 'git://github.com/cheald/manticore.git'
6
6
  #gem 'typhoeus'
7
- gem 'coveralls', :require => false
8
7
  gem 'httparty'
9
8
  gem 'rspec'
10
9
  gem 'rake'
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Wemote [![Build Status](https://travis-ci.org/gisikw/wemote.png)](https://travis-ci.org/gisikw/wemote) [![Gem Version](https://badge.fury.io/rb/wemote.png)](http://badge.fury.io/rb/wemote) [![Coverage Status](https://coveralls.io/repos/gisikw/wemote/badge.png)](https://coveralls.io/r/gisikw/wemote) [![Code Climate](https://codeclimate.com/github/gisikw/wemote.png)](https://codeclimate.com/github/gisikw/wemote)
1
+ # Wemote [![Build Status](https://travis-ci.org/gisikw/wemote.png)](https://travis-ci.org/gisikw/wemote) [![Gem Version](https://badge.fury.io/rb/wemote.png)](http://badge.fury.io/rb/wemote) [![Code Climate](https://codeclimate.com/github/gisikw/wemote.png)](https://codeclimate.com/github/gisikw/wemote)
2
2
 
3
3
  Wemote is an interface for controlling WeMo light switches (and possibly outlets in the future). Unlike other implementations, it does not rely upon `playful` for upnp discovery, which makes it compatible with alternative Ruby engines, such as JRuby.
4
4
 
@@ -21,18 +21,17 @@ Or install it yourself as:
21
21
  You can fetch all lightswitches on your network (providing you've set them up with your smartphone), via:
22
22
 
23
23
  ```ruby
24
- switches = Wemote::Switch.all(force_reload=false) #=> [#<Remo::Switch:0x27f33aef @host="192.168.1.11", @name="Kitchen Switch", @port="49154">, #<Remo::Switch:0x51a23566 @host="192.168.1.12", @name="Main Room", @port="49154">, #<Remo::Switch:0x705fe568 @host="192.168.1.10", @name="Bathroom Switch", @port="49153">]
24
+ switches = Wemote::Switch.all #=> [#<Wemote::Switch:0x27f33aef @host="192.168.1.11", @name="Kitchen Switch", @port="49154">
25
25
  ```
26
26
 
27
27
  Or select a switch by its friendly name:
28
28
 
29
29
  ```ruby
30
- switch = Wemote::Switch.find('Kitchen Switch') #=> #<Remo::Switch:0x27f33aef @host="192.168.1.11", @name="Kitchen Switch", @port="49154">
30
+ switch = Wemote::Switch.find('Kitchen Switch') #=> #<Wemote::Switch:0x27f33aef @host="192.168.1.11", @name="Kitchen Switch", @port="49154">
31
31
  ```
32
32
 
33
33
  Given a Switch instance, you can call the following methods:
34
34
  ```ruby
35
- switch.get_state #=> [:off,:on]
36
35
  switch.off? #=> [true,false]
37
36
  switch.on? #=> [true,false]
38
37
  switch.on!
@@ -42,7 +41,7 @@ switch.toggle!
42
41
 
43
42
  ## Performance
44
43
 
45
- Wemote 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.
44
+ Wemote 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`, 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.
46
45
 
47
46
  ## Contributing
48
47
 
@@ -1,7 +1,16 @@
1
1
  require_relative './wemote/version'
2
2
 
3
3
  module Wemote
4
+ require_relative './wemote/collection/switch'
4
5
  require_relative './wemote/switch'
5
6
  require_relative './wemote/client'
6
7
  require_relative './wemote/xml'
8
+
9
+ class << self
10
+
11
+ def discover(broadcast='255.255.255.255')
12
+ Wemote::Switch.send(:discover,broadcast)
13
+ end
14
+
15
+ end
7
16
  end
@@ -6,6 +6,7 @@ module Wemote
6
6
  constants.collect {|const_name| const_get(const_name)}.select {|const| const.class == Module}.detect do |mod|
7
7
  fulfilled = false
8
8
  begin
9
+ next unless mod.const_defined?(:DEPENDENCIES)
9
10
  mod.const_get(:DEPENDENCIES).map{|d|require d}
10
11
  fulfilled = true
11
12
  rescue LoadError
@@ -15,63 +16,59 @@ module Wemote
15
16
  end
16
17
  end
17
18
 
18
- module Manticore
19
- DEPENDENCIES = ['manticore']
20
-
21
- def get(*args)
22
- _get(::Manticore,*args).call
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
23
24
  end
25
+ end
24
26
 
25
- def post(*args)
26
- _post(::Manticore,*args).call
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)
27
33
  end
28
-
29
34
  end
30
35
 
31
36
  module Typhoeus
32
37
  DEPENDENCIES = ['typhoeus']
33
-
34
- def get(*args)
35
- _get(::Typhoeus,*args)
36
- end
37
-
38
- def post(*args)
39
- _post(::Typhoeus,*args)
38
+ def self.extended(base)
39
+ base.instance_variable_set(:@lib,::Typhoeus)
40
+ base.extend(SmartLib)
40
41
  end
41
-
42
42
  end
43
43
 
44
- module HTTParty
45
- DEPENDENCIES = ['httparty']
46
-
47
- def get(*args)
48
- _get(::HTTParty,*args)
49
- end
44
+ # HTTParty is temporarily disabled, as it's auto-parsing the XML
50
45
 
51
- def post(*args)
52
- _post(::HTTParty,*args)
53
- end
54
- end
46
+ #module HTTParty
47
+ # DEPENDENCIES = ['httparty']
48
+ # def self.extended(base)
49
+ # base.instance_variable_set(:@lib,::HTTParty)
50
+ # base.extend(SmartLib)
51
+ # end
52
+ #end
55
53
 
56
54
  module NetHTTP
57
55
  DEPENDENCIES = ['net/http','uri']
58
56
 
59
- def get(url,body=nil,headers=nil)
57
+ def request(klass,url,body=nil,headers=nil)
60
58
  uri = URI.parse(url)
61
59
  http = Net::HTTP.new(uri.host, uri.port)
62
- request = Net::HTTP::Get.new(uri.request_uri)
60
+ request = klass.new(uri.request_uri)
63
61
  headers.map{|k,v|request[k]=v} if headers
62
+ (request.body = body) if body
64
63
  response = http.request(request)
65
64
  end
66
65
 
67
- def post(url,body=nil,headers=nil)
68
- uri = URI.parse(url)
69
- http = Net::HTTP.new(uri.host, uri.port)
70
- request = Net::HTTP::Post.new(uri.request_uri)
71
- headers.map{|k,v|request[k]=v} if headers
72
- (request.body = body) if body
73
- response = http.request(request)
66
+ %w{get post put}.each do |name|
67
+ define_method name do |*args|
68
+ request(Net::HTTP.const_get(name.capitalize),*args)
69
+ end
74
70
  end
71
+
75
72
  end
76
73
 
77
74
  def initialize
@@ -80,12 +77,8 @@ module Wemote
80
77
 
81
78
  private
82
79
 
83
- def _get(lib,url,body=nil,headers=nil)
84
- lib.get(url,{body:body,headers:headers})
85
- end
86
-
87
- def _post(lib,url,body=nil,headers=nil)
88
- lib.post(url,{body:body,headers:headers})
80
+ def _req(lib,method,url,body=nil,headers=nil)
81
+ lib.send(method,url,{body:body,headers:headers})
89
82
  end
90
83
 
91
84
  end
@@ -0,0 +1,31 @@
1
+ module Wemote
2
+ module Collection
3
+
4
+ # This class provides an extension on the basic Array object, in order to
5
+ # facilitate group operations on a collection of Wemote::Switch instances.
6
+ class Switch < Array
7
+
8
+ class << self
9
+
10
+ private
11
+
12
+ # @macro [attach] container.increment
13
+ # @method $1()
14
+ # Calls {Wemote::Switch#$1} on all containing elements and return the results in an
15
+ # array
16
+ # @return [Array]
17
+ def make(name)
18
+ define_method(name){map{|s|s.send(name)}}
19
+ end
20
+
21
+ end
22
+
23
+ make :toggle!
24
+ make :off!
25
+ make :off?
26
+ make :on!
27
+ make :on?
28
+
29
+ end
30
+ end
31
+ end
@@ -1,19 +1,14 @@
1
1
  require 'socket'
2
2
  require 'ipaddr'
3
+ require 'timeout'
3
4
 
4
5
  module Wemote
6
+
7
+ # This class encapsulates an individual Wemo Switch. It provides methods for
8
+ # getting and setting the switch's state, as well as a {#toggle!} method for
9
+ # convenience. Finally, it provides the {#poll} method, which accepts a block
10
+ # to be executed any time the switch changes state.
5
11
  class Switch
6
- MULTICAST_ADDR = '239.255.255.250'
7
- BIND_ADDR = '0.0.0.0'
8
- PORT = 1900
9
- DISCOVERY= <<-EOF
10
- M-SEARCH * HTTP/1.1\r
11
- HOST: 239.255.255.250:1900\r
12
- MAN: "ssdp:discover"\r
13
- MX: 10\r
14
- ST: urn:Belkin:device:lightswitch:1\r
15
- \r
16
- EOF
17
12
 
18
13
  GET_HEADERS = {
19
14
  "SOAPACTION" => '"urn:Belkin:service:basicevent:1#GetBinaryState"',
@@ -26,71 +21,95 @@ EOF
26
21
  }
27
22
 
28
23
  class << self
24
+ # Returns all Switches detected on the local network
25
+ #
26
+ # @param [Boolean] refresh Refresh and redetect Switches
27
+ # @return [Array] all Switches on the network
29
28
  def all(refresh=false)
30
29
  @switches = nil if refresh
31
- @switches ||= fetch_switches
30
+ @switches ||= Wemote::Collection::Switch.new(discover)
32
31
  end
33
32
 
33
+ # Returns a Switch of a given name
34
+ #
35
+ # @param name [String] the friendly name of the Switch
36
+ # @return [Wemote::Switch] a Switch object
34
37
  def find(name)
35
38
  all.detect{|s|s.name == name}
36
39
  end
37
40
 
38
41
  private
39
42
 
40
- def ssdp_socket
41
- socket = UDPSocket.new
42
- socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, IPAddr.new(MULTICAST_ADDR).hton + IPAddr.new(BIND_ADDR).hton)
43
- socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, 1)
44
- socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEPORT, 1)
45
- socket.bind(BIND_ADDR,PORT)
46
- socket
47
- end
48
-
49
- def fetch_switches
50
- socket = ssdp_socket
51
- switches = []
52
-
53
- 3.times { socket.send(DISCOVERY, 0, MULTICAST_ADDR, PORT) }
54
-
55
- # The following is a bit silly, but is necessary for JRuby support,
56
- # which seems to have some issues with socket interruption.
57
- # If you have a working JRuby solution that doesn't require this
58
- # kind of hackery, by all means, submit a pull request!
59
-
60
- sleep 1
61
-
62
- parser = Thread.start do
63
- loop do
64
- message, _ = socket.recvfrom(1024)
65
- if message.match(/LOCATION.*Belkin/m)
66
- switches << message.match(/LOCATION:\s+http:\/\/([^\/]+)/)[1].split(':')
67
- end
68
- end
69
- end
70
-
71
- sleep 1
72
- parser.kill
73
-
74
- socket.close
75
-
76
- return switches.uniq.map{|s|self.new(*s)}
77
-
43
+ def discover(broadcast = '255.255.255.255')
44
+ `ping -t 1 #{broadcast} > /dev/null && arp -na | grep b4:75`.split("\n").map do |device|
45
+ self.new(/\((\d+\.\d+\.\d+\.\d+)\)/.match(device)[1])
46
+ end.reject{|device| device.instance_variable_get(:@port).nil? }
78
47
  end
79
48
  end
80
49
 
81
50
  attr_accessor :name
82
51
 
83
- def initialize(host,port)
52
+ def initialize(host,port=nil)
84
53
  @host, @port = host, port
85
54
  set_meta
86
55
  end
87
56
 
57
+ # Turn the Switch on or off, based on its current state
88
58
  def toggle!; on? ? off! : on!; end
59
+
60
+ # Turn the Switch off
89
61
  def off!; set_state(0); end
90
- def off?; get_state == :off; end
62
+
63
+ # Turn the Switch on
91
64
  def on!; set_state(1); end
65
+
66
+ # Return whether the Switch is off
67
+ #
68
+ # @return [Boolean]
69
+ def off?; get_state == :off; end
70
+
71
+ # Return whether the Switch is on
72
+ #
73
+ # @return [Boolean]
92
74
  def on?; get_state == :on; end
93
75
 
76
+ # Monitors the state of the Switch via polling, and yields to the block
77
+ # given with the updated state.
78
+ #
79
+ # @example Output when a Switch changes state
80
+ # light.poll do |state|
81
+ # if state == :on
82
+ # puts "The switch turned on"
83
+ # else
84
+ # puts "The switch turned off"
85
+ # end
86
+ # end
87
+ #
88
+ # @param rate [Float] The rate in seconds at which to poll the switch
89
+ # @param async [Boolean] Whether or not to poll the switch in a separate thread
90
+ #
91
+ # @return [Thread] if the method call was asynchronous
92
+ def poll(rate=0.25,async=true,&block)
93
+ old_state = get_state
94
+ poller = Thread.start do
95
+ loop do
96
+ begin
97
+ state = get_state
98
+ if state != old_state
99
+ old_state = state
100
+ yield state
101
+ end
102
+ rescue Exception
103
+ end
104
+ sleep rate
105
+ end
106
+ end
107
+ puts "Monitoring #{@name} for changes"
108
+ async ? poller : poller.join
109
+ end
110
+
111
+ private
112
+
94
113
  def get_state
95
114
  response = begin
96
115
  client.post("http://#{@host}:#{@port}/upnp/control/basicevent1",Wemote::XML.get_binary_state,GET_HEADERS)
@@ -108,15 +127,26 @@ EOF
108
127
  end
109
128
  end
110
129
 
111
- private
112
-
113
130
  def client
114
131
  @client ||= Wemote::Client.new
115
132
  end
116
133
 
117
134
  def set_meta
118
- response = client.get("http://#{@host}:#{@port}/setup.xml")
119
- @name = response.body.match(/<friendlyName>([^<]+)<\/friendlyName>/)[1]
135
+ if @port
136
+ response = client.get("http://#{@host}:#{@port}/setup.xml")
137
+ @name = response.body.match(/<friendlyName>([^<]+)<\/friendlyName>/)[1]
138
+ else
139
+ for port in 49152..49156
140
+ begin
141
+ response = nil
142
+ Timeout::timeout(1){ response = client.get("http://#{@host}:#{port}/setup.xml") }
143
+ @name = response.body.match(/<friendlyName>([^<]+)<\/friendlyName>/)[1]
144
+ @port = port
145
+ break
146
+ rescue Exception
147
+ end
148
+ end
149
+ end
120
150
  end
121
151
 
122
152
  end
@@ -1,3 +1,3 @@
1
1
  module Wemote
2
- VERSION = "0.2.0"
2
+ VERSION = "0.2.1"
3
3
  end
@@ -1,22 +1,23 @@
1
1
  module Wemote
2
2
  module XML
3
3
  class << self
4
- TEMPLATES = {}
4
+ xml_path = File.join(File.dirname(__FILE__),'/../../xml')
5
+ TEMPLATES = {
6
+ get_binary_state: File.read(File.join(xml_path,'get_binary_state.xml')),
7
+ set_binary_state: File.read(File.join(xml_path,'set_binary_state.xml'))
8
+ }
5
9
 
6
- Dir.glob(File.join(File.dirname(__FILE__),'/../../xml/*.xml')).each do |file|
7
- basename = File.basename(file,'.xml')
8
- TEMPLATES[basename] = File.read(file)
9
-
10
- define_method basename do |*args|
11
- t = TEMPLATES[basename].dup
12
- if (replace = t.scan(/{{\d+}}/).size) != args.size
13
- raise ArgumentError, "wrong number of arguments calling `#{basename}` (#{args.size} for #{replace})"
14
- end
15
- (1..replace).map{|i|t.gsub!("{{#{i}}}",args[i-1].to_s)}
16
- t
17
- end
10
+ # @return [String] The required XML body for a Wemo binary state request
11
+ def get_binary_state
12
+ TEMPLATES[:get_binary_state]
13
+ end
18
14
 
15
+ # @param [Integer] state Either 1 or 0, for off and on respectively
16
+ # @return [String] The required XML body for a Wemo binary state set request
17
+ def set_binary_state(state)
18
+ TEMPLATES[:set_binary_state].gsub("{{1}}",state.to_s)
19
19
  end
20
+
20
21
  end
21
22
  end
22
23
  end
@@ -1,4 +1 @@
1
- require 'coveralls'
2
- Coveralls.wear!
3
-
4
1
  require 'wemote'
@@ -72,22 +72,5 @@ describe Wemote::Switch do
72
72
  end
73
73
  end
74
74
 
75
- describe 'get_state' do
76
- it 'should return the binary state of the switch' do
77
-
78
- @switch.client.stub(:post).and_return(double('response',body:'<BinaryState>1</BinaryState>'))
79
- @switch.get_state.should == :on
80
- @switch.client.stub(:post).and_return(double('response',body:'<BinaryState>0</BinaryState>'))
81
- @switch.get_state.should == :off
82
- end
83
- end
84
-
85
- describe 'set_state' do
86
- it 'should set the binary state of the switch' do
87
- @switch.client.stub(:post).and_return(double('response',body:'Called'))
88
- @switch.set_state(1).body.should == 'Called'
89
- end
90
- end
91
-
92
75
  end
93
76
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wemote
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Gisi
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-03-06 00:00:00.000000000 Z
11
+ date: 2014-08-08 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Wemote is a Ruby-agnostic gem for Wemo light switches
14
14
  email:
@@ -17,15 +17,15 @@ executables: []
17
17
  extensions: []
18
18
  extra_rdoc_files: []
19
19
  files:
20
- - .coveralls.yml
21
- - .gitignore
22
- - .travis.yml
20
+ - ".gitignore"
21
+ - ".travis.yml"
23
22
  - Gemfile
24
23
  - LICENSE.txt
25
24
  - README.md
26
25
  - Rakefile
27
26
  - lib/wemote.rb
28
27
  - lib/wemote/client.rb
28
+ - lib/wemote/collection/switch.rb
29
29
  - lib/wemote/switch.rb
30
30
  - lib/wemote/version.rb
31
31
  - lib/wemote/xml.rb
@@ -38,26 +38,27 @@ homepage: https://github.com/gisikw/wemote
38
38
  licenses:
39
39
  - MIT
40
40
  metadata: {}
41
- post_install_message:
41
+ post_install_message:
42
42
  rdoc_options: []
43
43
  require_paths:
44
44
  - lib
45
45
  required_ruby_version: !ruby/object:Gem::Requirement
46
46
  requirements:
47
- - - '>='
47
+ - - ">="
48
48
  - !ruby/object:Gem::Version
49
49
  version: '0'
50
50
  required_rubygems_version: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - '>='
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  requirements: []
56
- rubyforge_project:
56
+ rubyforge_project:
57
57
  rubygems_version: 2.2.2
58
- signing_key:
58
+ signing_key:
59
59
  specification_version: 4
60
60
  summary: Wemote is a Ruby-agnostic gem for Wemo light switches
61
61
  test_files:
62
62
  - spec/spec_helper.rb
63
63
  - spec/wemote/switch_spec.rb
64
+ has_rdoc:
@@ -1 +0,0 @@
1
- service_name: travis-ci