wemote 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e8691a6386c98200751335860a3601d8377df98a
4
+ data.tar.gz: c24270bd139e1065b1a7e2e33695629241397bc1
5
+ SHA512:
6
+ metadata.gz: ae867770e792ab4ab14a25147a94f8f18bd5ebd4297cdce837bc78c41bc286dc20d11dd24267336680001fecddf2822ab8dc03d74f3ab431b8b57d792c65a1e9
7
+ data.tar.gz: 3f9008db4572c37f11c4fd0a6082e55afae6a827fb82cb6250eb27fb0351a447d464de2424ccadcb9a65a24d2b0c2471449fac515546ab3931f5892f7e19774c
@@ -0,0 +1,20 @@
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
18
+ *.swp
19
+ *.swo
20
+ *~
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - jruby-head
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in manticore.gemspec
4
+ gemspec
5
+
6
+ gem 'coveralls', require: false
7
+ gem 'manticore', git: 'git://github.com/cheald/manticore.git'
8
+ gem 'rspec'
9
+ gem 'rake'
@@ -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.
@@ -0,0 +1,49 @@
1
+ # Wemote [![Build Status](https://travis-ci.org/gisikw/wemote.png)](https://travis-ci.org/gisikw/wemote)[![Coverage Status](https://coveralls.io/repos/gisikw/wemote/badge.png)](https://coveralls.io/r/gisikw/wemote)
2
+
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 JRuby. For the moment, Wemote leverages Manticore for its HTTP requests, and until that is abstracted, this library is only compatible with JRuby.
4
+
5
+ ## Installation
6
+
7
+ First, clone the repository:
8
+
9
+ git clone https://github.com/gisikw/wemote.git
10
+
11
+ And then execute:
12
+
13
+ $ gem build wemote.gemspec
14
+
15
+ Finally, run:
16
+
17
+ $ gem install wemote-0.0.1.gem
18
+
19
+ ## Usage
20
+
21
+ You can fetch all lightswitches on your network (providing you've set them up with your smartphone), via:
22
+
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">]
25
+ ```
26
+
27
+ Or select a switch by its friendly name:
28
+
29
+ ```ruby
30
+ switch = Wemote::Switch.find('Kitchen Switch') #=> #<Remo::Switch:0x27f33aef @host="192.168.1.11", @name="Kitchen Switch", @port="49154">
31
+ ```
32
+
33
+ Given a Switch instance, you can call the following methods:
34
+ ```ruby
35
+ switch.state #=> ["off","on"]
36
+ switch.off? #=> [true,false]
37
+ switch.on? #=> [true,false]
38
+ switch.on!
39
+ switch.off!
40
+ switch.toggle!
41
+ ```
42
+
43
+ ## Contributing
44
+
45
+ 1. Fork it
46
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
47
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
48
+ 4. Push to the branch (`git push origin my-new-feature`)
49
+ 5. Create new Pull Request
@@ -0,0 +1,8 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ RSpec::Core::RakeTask.new(:spec) do |spec|
4
+ spec.pattern = 'spec/**/*_spec.rb'
5
+ spec.rspec_opts = ['--tty --color --format documentation']
6
+ end
7
+
8
+ task :default => :spec
@@ -0,0 +1,7 @@
1
+ require 'manticore'
2
+ require_relative './wemote/version'
3
+
4
+ module Wemote
5
+ require_relative './wemote/switch'
6
+ require_relative './wemote/xml'
7
+ end
@@ -0,0 +1,117 @@
1
+ require 'socket'
2
+ require 'ipaddr'
3
+
4
+ module Wemote
5
+ 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
+
18
+ GET_HEADERS = {
19
+ "SOAPACTION" => '"urn:Belkin:service:basicevent:1#GetBinaryState"',
20
+ "Content-type" => 'text/xml; charset="utf-8"'
21
+ }
22
+
23
+ SET_HEADERS = {
24
+ "SOAPACTION" => '"urn:Belkin:service:basicevent:1#SetBinaryState"',
25
+ "Content-type" => 'text/xml; charset="utf-8"'
26
+ }
27
+
28
+ class << self
29
+ def all(refresh=false)
30
+ @switches = nil if refresh
31
+ @switches ||= fetch_switches
32
+ end
33
+
34
+ def find(name)
35
+ all.detect{|s|s.name == name}
36
+ end
37
+
38
+ private
39
+
40
+ def fetch_switches
41
+ socket = UDPSocket.new
42
+ membership = IPAddr.new(MULTICAST_ADDR).hton + IPAddr.new(BIND_ADDR).hton
43
+
44
+ socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, membership)
45
+ socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, 1)
46
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEPORT, 1)
47
+ socket.bind(BIND_ADDR,PORT)
48
+
49
+ switches = []
50
+
51
+ 3.times { socket.send(DISCOVERY, 0, MULTICAST_ADDR, PORT) }
52
+
53
+ sleep 1
54
+
55
+ parser = Thread.start do
56
+ loop do
57
+ message, _ = socket.recvfrom(1024)
58
+ if message.match(/LOCATION.*Belkin/m)
59
+ switches << message.match(/LOCATION:\s+http:\/\/([^\/]+)/)[1].split(':')
60
+ end
61
+ end
62
+ end
63
+
64
+ sleep 1
65
+ parser.kill
66
+
67
+ socket.close
68
+
69
+ return switches.uniq.map{|s|self.new(*s)}
70
+
71
+ end
72
+ end
73
+
74
+ attr_accessor :name
75
+
76
+ def initialize(host,port)
77
+ @host, @port = host, port
78
+ set_meta
79
+ end
80
+
81
+ def toggle!; on? ? off! : on!; end
82
+ def off!; set_state(0); end
83
+ def off?; get_state == :off; end
84
+ def on!; set_state(1); end
85
+ def on?; get_state == :on; end
86
+
87
+ def get_state
88
+ response = begin
89
+ client.post("http://#{@host}:#{@port}/upnp/control/basicevent1",:body => Wemote::XML.get_binary_state, :headers => GET_HEADERS).call
90
+ rescue Exception
91
+ client.post("http://#{@host}:#{@port}/upnp/control/basicevent1",:body => Wemote::XML.get_binary_state, :headers => GET_HEADERS).call
92
+ end
93
+ response.body.match(/<BinaryState>(\d)<\/BinaryState>/)[1] == '1' ? :on : :off
94
+ end
95
+
96
+ def set_state(state)
97
+ begin
98
+ client.post("http://#{@host}:#{@port}/upnp/control/basicevent1",:body => Wemote::XML.set_binary_state(state), :headers => SET_HEADERS).call
99
+ rescue Exception
100
+ client.post("http://#{@host}:#{@port}/upnp/control/basicevent1",:body => Wemote::XML.set_binary_state(state), :headers => SET_HEADERS).call
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ def client
107
+ @client ||= Manticore::Client.new
108
+ end
109
+
110
+ def set_meta
111
+ client.get("http://#{@host}:#{@port}/setup.xml") do |response|
112
+ @name = response.body.match(/<friendlyName>([^<]+)<\/friendlyName>/)[1]
113
+ end
114
+ end
115
+
116
+ end
117
+ end
@@ -0,0 +1,3 @@
1
+ module Wemote
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,22 @@
1
+ module Wemote
2
+ module XML
3
+ class << self
4
+ TEMPLATES = {}
5
+
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
18
+
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,4 @@
1
+ require 'wemote'
2
+ require 'coveralls'
3
+
4
+ Coveralls.wear!
@@ -0,0 +1,92 @@
1
+ require 'spec_helper'
2
+
3
+ describe Wemote::Switch do
4
+ describe 'class methods' do
5
+
6
+ describe '.all' do
7
+ it 'should have specs'
8
+ end
9
+
10
+ describe '.find' do
11
+ it 'should return an instance whose name matches the argument' do
12
+ client = Manticore::Client.new
13
+ client.stub('http://fakehost:1234/setup.xml',body:'<friendlyName>Test Switch</friendlyName>',code:200)
14
+ Wemote::Switch.any_instance.stub(:client).and_return(client)
15
+ switch = Wemote::Switch.new('fakehost','1234')
16
+ Wemote::Switch.instance_variable_set(:@switches,[switch])
17
+ Wemote::Switch.find('Test Switch').should == switch
18
+ end
19
+ end
20
+
21
+ end
22
+
23
+ describe 'instance methods' do
24
+ before do
25
+ client = Manticore::Client.new
26
+ client.stub('http://fakehost:1234/setup.xml',body:'<friendlyName>Test Switch</friendlyName>',code:200)
27
+ Wemote::Switch.any_instance.stub(:client).and_return(client)
28
+ @switch = Wemote::Switch.new('fakehost','1234')
29
+ end
30
+
31
+ describe '#initialize' do
32
+ it 'should set the host and port from the arguments' do
33
+ @switch.instance_variable_get(:@host).should == 'fakehost'
34
+ @switch.instance_variable_get(:@port).should == '1234'
35
+ end
36
+
37
+ it 'should set the friendly name of the switch' do
38
+ @switch.name.should == 'Test Switch'
39
+ end
40
+
41
+ end
42
+
43
+ describe 'on!' do
44
+ it 'should call #set_state with an argument of 1' do
45
+ expect(@switch).to receive(:set_state).with(1)
46
+ @switch.on!
47
+ end
48
+ end
49
+
50
+ describe 'off!' do
51
+ it 'should call #set_state with an argument of 0' do
52
+ expect(@switch).to receive(:set_state).with(0)
53
+ @switch.off!
54
+ end
55
+ end
56
+
57
+ describe 'on?' do
58
+ it 'should return whether the switch state is on' do
59
+ @switch.stub(:get_state).and_return(:on)
60
+ @switch.on?.should == true
61
+ @switch.stub(:get_state).and_return(:off)
62
+ @switch.on?.should == false
63
+ end
64
+ end
65
+
66
+ describe 'off?' do
67
+ it 'should return whether the switch state is off' do
68
+ @switch.stub(:get_state).and_return(:on)
69
+ @switch.off?.should == false
70
+ @switch.stub(:get_state).and_return(:off)
71
+ @switch.off?.should == true
72
+ end
73
+ end
74
+
75
+ describe 'get_state' do
76
+ it 'should return the binary state of the switch' do
77
+ @switch.client.stub('http://fakehost:1234/upnp/control/basicevent1',body:'<BinaryState>1</BinaryState>',code:200)
78
+ @switch.get_state.should == :on
79
+ @switch.client.stub('http://fakehost:1234/upnp/control/basicevent1',body:'<BinaryState>0</BinaryState>',code:200)
80
+ @switch.get_state.should == :off
81
+ end
82
+ end
83
+
84
+ describe 'set_state' do
85
+ it 'should set the binary state of the switch' do
86
+ @switch.client.stub('http://fakehost:1234/upnp/control/basicevent1',body:'Called',code:200)
87
+ @switch.set_state(1).body.should == 'Called'
88
+ end
89
+ end
90
+
91
+ end
92
+ end
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'wemote/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "wemote"
8
+ spec.version = Wemote::VERSION
9
+ spec.authors = ["Kevin Gisi"]
10
+ spec.email = ["kevin@kevingisi.com"]
11
+ spec.description = %q{Wemote is a JRuby-friendly API for Wemo light switches}
12
+ spec.summary = %q{Wemote is a JRuby-friendly API for Wemo light switches}
13
+ spec.homepage = "https://github.com/gisikw/wemote"
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
+
22
+ spec.add_dependency "manticore", "~> 0.2.1"
23
+ end
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
3
+ s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
4
+ <s:Body>
5
+ <u:GetBinaryState xmlns:u="urn:Belkin:service:basicevent:1"/>
6
+ </s:Body>
7
+ </s:Envelope>
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
3
+ s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
4
+ <s:Body>
5
+ <u:SetBinaryState xmlns:u="urn:Belkin:service:basicevent:1">
6
+ <BinaryState>{{1}}</BinaryState>
7
+ </u:SetBinaryState>
8
+ </s:Body>
9
+ </s:Envelope>
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: wemote
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kevin Gisi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-03-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: manticore
15
+ version_requirements: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 0.2.1
20
+ requirement: !ruby/object:Gem::Requirement
21
+ requirements:
22
+ - - ~>
23
+ - !ruby/object:Gem::Version
24
+ version: 0.2.1
25
+ prerelease: false
26
+ type: :runtime
27
+ description: Wemote is a JRuby-friendly API for Wemo light switches
28
+ email:
29
+ - kevin@kevingisi.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - .gitignore
35
+ - .travis.yml
36
+ - Gemfile
37
+ - LICENSE.txt
38
+ - README.md
39
+ - Rakefile
40
+ - lib/wemote.rb
41
+ - lib/wemote/switch.rb
42
+ - lib/wemote/version.rb
43
+ - lib/wemote/xml.rb
44
+ - spec/spec_helper.rb
45
+ - spec/wemote/switch_spec.rb
46
+ - wemote.gemspec
47
+ - xml/get_binary_state.xml
48
+ - xml/set_binary_state.xml
49
+ homepage: https://github.com/gisikw/wemote
50
+ licenses:
51
+ - MIT
52
+ metadata: {}
53
+ post_install_message:
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - '>='
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubyforge_project:
69
+ rubygems_version: 2.2.2
70
+ signing_key:
71
+ specification_version: 4
72
+ summary: Wemote is a JRuby-friendly API for Wemo light switches
73
+ test_files:
74
+ - spec/spec_helper.rb
75
+ - spec/wemote/switch_spec.rb