sonos 0.1.1 → 0.2.0
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.
- data/Changelog.markdown +24 -17
- data/Readme.markdown +22 -7
- data/bin/sonos +7 -0
- data/lib/sonos.rb +5 -0
- data/lib/sonos/cli.rb +20 -0
- data/lib/sonos/discovery.rb +73 -0
- data/lib/sonos/endpoint.rb +9 -0
- data/lib/sonos/endpoint/content_directory.rb +53 -0
- data/lib/sonos/endpoint/device.rb +23 -0
- data/lib/sonos/endpoint/rendering.rb +99 -0
- data/lib/sonos/endpoint/transport.rb +87 -0
- data/lib/sonos/speaker.rb +9 -9
- data/lib/sonos/topology.rb +35 -0
- data/lib/sonos/version.rb +1 -1
- data/sonos.gemspec +3 -2
- metadata +32 -8
- data/lib/sonos/content_directory.rb +0 -55
- data/lib/sonos/device.rb +0 -25
- data/lib/sonos/rendering.rb +0 -88
- data/lib/sonos/transport.rb +0 -89
data/Changelog.markdown
CHANGED
|
@@ -1,28 +1,35 @@
|
|
|
1
|
-
### Version 0.
|
|
1
|
+
### Version 0.2.0 — December 24, 2012
|
|
2
2
|
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* Add Loudness setting
|
|
4
|
+
* Moved `sonos-discover` to `sonos discover`
|
|
5
|
+
* Moved `sonos-discover-multiple` to `sonos discover --all`
|
|
6
|
+
* Cleaned up enpoints
|
|
7
|
+
|
|
8
|
+
### Version 0.1.1 — December 24, 2012
|
|
9
|
+
|
|
10
|
+
* Add Queue
|
|
11
|
+
* Add Clear Queue
|
|
12
|
+
* Add Save Queue
|
|
13
|
+
* Add Device Description URL
|
|
7
14
|
|
|
8
15
|
### Version 0.1.0 — December 24, 2012
|
|
9
16
|
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
+
* Add Stop
|
|
18
|
+
* Add Next
|
|
19
|
+
* Add Previous
|
|
20
|
+
* Add Mute
|
|
21
|
+
* Add Status Light
|
|
22
|
+
* Add Bass
|
|
23
|
+
* Add Treble
|
|
17
24
|
* Remove some method aliases
|
|
18
25
|
|
|
19
26
|
### Version 0.0.1 — December 23, 2012
|
|
20
27
|
|
|
21
28
|
Initial release.
|
|
22
29
|
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
30
|
+
* Add Volume
|
|
31
|
+
* Add Play
|
|
32
|
+
* Add Play Stream
|
|
33
|
+
* Add Pause
|
|
34
|
+
* Add Now Playing
|
|
28
35
|
|
data/Readme.markdown
CHANGED
|
@@ -26,7 +26,7 @@ $ gem install sonos
|
|
|
26
26
|
|
|
27
27
|
## Usage
|
|
28
28
|
|
|
29
|
-
I'm working on a CLI client. For now, we'll use IRB.
|
|
29
|
+
I'm working on a CLI client. For now, we'll use IRB.
|
|
30
30
|
|
|
31
31
|
``` shell
|
|
32
32
|
$ gem install sonos
|
|
@@ -36,7 +36,7 @@ $ irb
|
|
|
36
36
|
``` ruby
|
|
37
37
|
require 'rubygems'
|
|
38
38
|
require 'sonos'
|
|
39
|
-
speaker = Sonos
|
|
39
|
+
speaker = Sonos.discover
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
Now that we have a reference to the speaker, we can do all kinds of stuff.
|
|
@@ -49,20 +49,35 @@ speaker.now_playing
|
|
|
49
49
|
speaker.volume
|
|
50
50
|
speaker.volume = 70
|
|
51
51
|
speaker.volume -= 10
|
|
52
|
+
speaker.queue
|
|
53
|
+
speaker.save_queue 'Jams'
|
|
54
|
+
speaker.clear_queue
|
|
52
55
|
```
|
|
53
56
|
|
|
57
|
+
### Topology
|
|
58
|
+
|
|
59
|
+
`Sonos.discover` finds the first speaker it can. We can get all of the Sonos devices (including Bridges, etc) by calling `speaker.topology`. This is going to get refactored a bit. Right now everything is nested under speaker which is kinda messy and confusing.
|
|
60
|
+
|
|
61
|
+
### CLI
|
|
62
|
+
|
|
63
|
+
There is a very limited CLI right now. You can run `sonos discover` to get the IP of one of your devices. Run `sonos discover --all` to get all of them.
|
|
64
|
+
|
|
54
65
|
## To Do
|
|
55
66
|
|
|
67
|
+
* Refactor all of the things
|
|
68
|
+
* Nonblocking calls with Celluloid::IO
|
|
69
|
+
* List other speakers
|
|
56
70
|
* Loudness
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
71
|
+
* Alarm clock
|
|
72
|
+
* Group management
|
|
73
|
+
* Party Mode
|
|
74
|
+
* Join
|
|
75
|
+
* Line-in (I don't have a PLAY:5, so I'll need help testing this one)
|
|
60
76
|
* Handle errors better
|
|
61
77
|
* Fix album art in `now_playing`
|
|
62
78
|
* Handle line-in in `now_playing`
|
|
63
|
-
* Auto-discovery
|
|
64
79
|
* Better support for stero pairs
|
|
65
|
-
* CLI client
|
|
80
|
+
* CLI client for everything
|
|
66
81
|
|
|
67
82
|
## Contributing
|
|
68
83
|
|
data/bin/sonos
ADDED
data/lib/sonos.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'sonos/version'
|
|
2
2
|
require 'sonos/speaker'
|
|
3
|
+
require 'sonos/discovery'
|
|
3
4
|
|
|
4
5
|
module Sonos
|
|
5
6
|
PORT = 1400
|
|
@@ -8,4 +9,8 @@ module Sonos
|
|
|
8
9
|
def self.Speaker(ip)
|
|
9
10
|
Speaker.new(ip)
|
|
10
11
|
end
|
|
12
|
+
|
|
13
|
+
def self.discover
|
|
14
|
+
Sonos::Discovery.new.discover
|
|
15
|
+
end
|
|
11
16
|
end
|
data/lib/sonos/cli.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require 'thor'
|
|
2
|
+
require 'sonos'
|
|
3
|
+
|
|
4
|
+
module Sonos
|
|
5
|
+
class Cli < Thor
|
|
6
|
+
desc 'discover', 'Finds the IP address of a Sonos device on your network'
|
|
7
|
+
method_option :all, type: :boolean, aliases: '-a', desc: 'Find all of the IP address instead of the first one discoverd'
|
|
8
|
+
def discover
|
|
9
|
+
speaker = Sonos.discover
|
|
10
|
+
|
|
11
|
+
if options[:all]
|
|
12
|
+
speaker.topology.each do |node|
|
|
13
|
+
puts "#{node.name.ljust(20)} #{node.ip}"
|
|
14
|
+
end
|
|
15
|
+
else
|
|
16
|
+
puts "#{speaker.zone_name.ljust(20)} #{speaker.ip}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
require 'socket'
|
|
2
|
+
require 'ipaddr'
|
|
3
|
+
require 'timeout'
|
|
4
|
+
|
|
5
|
+
#
|
|
6
|
+
# Inspired by https://github.com/rahims/SoCo, https://github.com/turboladen/upnp,
|
|
7
|
+
# and http://onestepback.org/index.cgi/Tech/Ruby/MulticastingInRuby.red.
|
|
8
|
+
#
|
|
9
|
+
# Turboladen's uPnP work is super-smart, but doesn't seem to work with 1.9.3 due to soap4r dep's.
|
|
10
|
+
#
|
|
11
|
+
# Some day this nonsense should be asynchronous / nonblocking / decorated with rainbows.
|
|
12
|
+
#
|
|
13
|
+
|
|
14
|
+
module Sonos
|
|
15
|
+
class Discovery
|
|
16
|
+
MULTICAST_ADDR = '239.255.255.250'
|
|
17
|
+
MULTICAST_PORT = 1900
|
|
18
|
+
DEFAULT_TIMEOUT = 1
|
|
19
|
+
|
|
20
|
+
attr_reader :timeout
|
|
21
|
+
|
|
22
|
+
def initialize(timeout = nil)
|
|
23
|
+
@timeout = timeout || DEFAULT_TIMEOUT
|
|
24
|
+
|
|
25
|
+
initialize_socket
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def discover
|
|
29
|
+
send_discovery_message
|
|
30
|
+
result = listen_for_responses
|
|
31
|
+
Sonos::Speaker.new(result) if result
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def send_discovery_message
|
|
37
|
+
# Request announcements
|
|
38
|
+
@socket.send(search_message, 0, MULTICAST_ADDR, MULTICAST_PORT)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def listen_for_responses
|
|
42
|
+
begin
|
|
43
|
+
Timeout::timeout(timeout) do
|
|
44
|
+
loop do
|
|
45
|
+
message, info = @socket.recvfrom(2048)
|
|
46
|
+
# return the IP address
|
|
47
|
+
return info[2]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
rescue Timeout::Error => ex
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def initialize_socket
|
|
56
|
+
# Create a socket
|
|
57
|
+
@socket = UDPSocket.open
|
|
58
|
+
|
|
59
|
+
# We're going to use IP with the multicast TTL. Mystery third parameter is a mystery.
|
|
60
|
+
@socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, 2)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def search_message
|
|
64
|
+
[
|
|
65
|
+
'M-SEARCH * HTTP/1.1',
|
|
66
|
+
"HOST: #{MULTICAST_ADDR}:reservedSSDPport",
|
|
67
|
+
'MAN: ssdp:discover',
|
|
68
|
+
"MX: #{timeout}",
|
|
69
|
+
"ST: urn:schemas-upnp-org:device:ZonePlayer:1"
|
|
70
|
+
].join("\n")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Sonos::Endpoint::ContentDirectory
|
|
2
|
+
CONTENT_DIRECTORY_ENDPOINT = '/MediaServer/ContentDirectory/Control'
|
|
3
|
+
CONTENT_DIRECTORY_XMLNS = 'urn:schemas-upnp-org:service:ContentDirectory:1'
|
|
4
|
+
|
|
5
|
+
# Get the current queue
|
|
6
|
+
def queue(starting_index = 0, requested_count = 100)
|
|
7
|
+
name = 'Browse'
|
|
8
|
+
action = "#{CONTENT_DIRECTORY_XMLNS}##{name}"
|
|
9
|
+
message = %Q{<u:#{name} xmlns:u="#{CONTENT_DIRECTORY_XMLNS}"><ObjectID>Q:0</ObjectID><BrowseFlag>BrowseDirectChildren</BrowseFlag><Filter>dc:title,res,dc:creator,upnp:artist,upnp:album,upnp:albumArtURI</Filter><StartingIndex>#{starting_index}</StartingIndex><RequestedCount>#{requested_count}</RequestedCount><SortCriteria></SortCriteria></u:Browse>}
|
|
10
|
+
result = content_directory_client.call name, soap_action: action, message: message
|
|
11
|
+
body = result.body[:browse_response]
|
|
12
|
+
|
|
13
|
+
hash = {
|
|
14
|
+
total: body[:total_matches].to_i,
|
|
15
|
+
items: parse_items(body[:result])
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# Paginate
|
|
19
|
+
# TODO: This is ugly and inflexible
|
|
20
|
+
if starting_index == 0
|
|
21
|
+
start = starting_index
|
|
22
|
+
while hash[:items].count < hash[:total]
|
|
23
|
+
start += requested_count
|
|
24
|
+
hash[:items] += browse(start, requested_count)[:items]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
hash
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def content_directory_client
|
|
34
|
+
@content_directory_client ||= Savon.client endpoint: "http://#{self.ip}:#{Sonos::PORT}#{CONTENT_DIRECTORY_ENDPOINT}", namespace: Sonos::NAMESPACE
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def parse_items(string)
|
|
38
|
+
result = []
|
|
39
|
+
doc = Nokogiri::XML(string)
|
|
40
|
+
doc.css('item').each do |item|
|
|
41
|
+
res = item.css('res').first
|
|
42
|
+
result << {
|
|
43
|
+
title: item.xpath('dc:title').inner_text,
|
|
44
|
+
artist: item.xpath('dc:creator').inner_text,
|
|
45
|
+
album: item.xpath('upnp:album').inner_text,
|
|
46
|
+
album_art: "http://#{self.ip}:#{PORT}#{item.xpath('upnp:albumArtURI').inner_text}",
|
|
47
|
+
duration: res['duration'],
|
|
48
|
+
id: res.inner_text
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
result
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Sonos::Endpoint::Device
|
|
2
|
+
DEVICE_ENDPOINT = '/DeviceProperties/Control'
|
|
3
|
+
DEVICE_XMLNS = 'urn:schemas-upnp-org:service:DeviceProperties:1'
|
|
4
|
+
|
|
5
|
+
# Turn the white status light on or off
|
|
6
|
+
# @param [Boolean] True to turn on the light. False to turn off the light.
|
|
7
|
+
def status_light_enabled=(enabled)
|
|
8
|
+
send_device_message('SetLEDState', enabled ? 'On' : 'Off')
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def device_client
|
|
14
|
+
@device_client ||= Savon.client endpoint: "http://#{self.ip}:#{Sonos::PORT}#{DEVICE_ENDPOINT}", namespace: Sonos::NAMESPACE
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def send_device_message(name, value)
|
|
18
|
+
action = "#{DEVICE_XMLNS}##{name}"
|
|
19
|
+
attribute = name.sub('Set', '')
|
|
20
|
+
message = %Q{<u:#{name} xmlns:u="#{DEVICE_XMLNS}"><Desired#{attribute}>#{value}</Desired#{attribute}>}
|
|
21
|
+
device_client.call(name, soap_action: action, message: message)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
module Sonos::Endpoint::Rendering
|
|
2
|
+
RENDERING_ENDPOINT = '/MediaRenderer/RenderingControl/Control'
|
|
3
|
+
RENDERING_XMLNS = 'urn:schemas-upnp-org:service:RenderingControl:1'
|
|
4
|
+
|
|
5
|
+
# Get the current volume.
|
|
6
|
+
# @return [Fixnum] the volume from 0 to 100
|
|
7
|
+
def volume
|
|
8
|
+
response = send_rendering_message('GetVolume')
|
|
9
|
+
response.body[:get_volume_response][:current_volume].to_i
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Set the volume from 0 to 100.
|
|
13
|
+
# @param [Fixnum] the desired volume from 0 to 100
|
|
14
|
+
def volume=(value)
|
|
15
|
+
send_rendering_message('SetVolume', value)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Get the current bass EQ.
|
|
19
|
+
# @return [Fixnum] the base EQ from -10 to 10
|
|
20
|
+
def bass
|
|
21
|
+
response = send_rendering_message('GetBass')
|
|
22
|
+
response.body[:get_bass_response][:current_bass].to_i
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Set the bass EQ from -10 to 10.
|
|
26
|
+
# @param [Fixnum] the desired bass EQ from -10 to 10
|
|
27
|
+
def bass=(value)
|
|
28
|
+
send_rendering_message('SetBass', value)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Get the current treble EQ.
|
|
32
|
+
# @return [Fixnum] the treble EQ from -10 to 10
|
|
33
|
+
def treble
|
|
34
|
+
response = send_rendering_message('GetTreble')
|
|
35
|
+
response.body[:get_treble_response][:current_treble].to_i
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Set the treble EQ from -10 to 10.
|
|
39
|
+
# @param [Fixnum] the desired treble EQ from -10 to 10
|
|
40
|
+
def treble=(value)
|
|
41
|
+
send_rendering_message('SetTreble', value)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Mute the speaker
|
|
45
|
+
def mute
|
|
46
|
+
set_mute(true)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Unmute the speaker
|
|
50
|
+
def unmute
|
|
51
|
+
set_mute(false)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Is the speaker muted?
|
|
55
|
+
# @return [Boolean] true if the speaker is muted and false if it is not
|
|
56
|
+
def muted?
|
|
57
|
+
response = send_rendering_message('GetMute')
|
|
58
|
+
response.body[:get_mute_response][:current_mute] == '1'
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Get the loudness compenstation setting
|
|
62
|
+
# @return [Boolean] true if the speaker has loudness on and false if it is not
|
|
63
|
+
def loudness
|
|
64
|
+
response = send_rendering_message('GetLoudness')
|
|
65
|
+
response.body[:get_loudness_response][:current_loudness] == '1'
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Set the loudness compenstation setting
|
|
69
|
+
# @param [Boolean] if the speaker has loudness on or not
|
|
70
|
+
def loudness=(value)
|
|
71
|
+
send_rendering_message('SetLoudness', value ? 1 : 0)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Sets the speaker's mute
|
|
77
|
+
# @param [Boolean] if the speaker is muted or not
|
|
78
|
+
def set_mute(value)
|
|
79
|
+
send_rendering_message('SetMute', value ? 1 : 0)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def rendering_client
|
|
83
|
+
@rendering_client ||= Savon.client endpoint: "http://#{self.ip}:#{Sonos::PORT}#{RENDERING_ENDPOINT}", namespace: Sonos::NAMESPACE
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def send_rendering_message(name, value = nil)
|
|
87
|
+
action = "#{RENDERING_XMLNS}##{name}"
|
|
88
|
+
message = %Q{<u:#{name} xmlns:u="#{RENDERING_XMLNS}"><InstanceID>0</InstanceID><Channel>Master</Channel>}
|
|
89
|
+
|
|
90
|
+
if value
|
|
91
|
+
attribute = name.sub('Set', '')
|
|
92
|
+
message += %Q{<Desired#{attribute}>#{value}</Desired#{attribute}></u:#{name}>}
|
|
93
|
+
else
|
|
94
|
+
message += %Q{</u:#{name}>}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
rendering_client.call(name, soap_action: action, message: message)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
module Sonos::Endpoint::Transport
|
|
2
|
+
TRANSPORT_ENDPOINT = '/MediaRenderer/AVTransport/Control'
|
|
3
|
+
TRANSPORT_XMLNS = 'urn:schemas-upnp-org:service:AVTransport:1'
|
|
4
|
+
|
|
5
|
+
# Get information about the currently playing track.
|
|
6
|
+
# @return [Hash] information about the current track.
|
|
7
|
+
def now_playing
|
|
8
|
+
response = send_transport_message('GetPositionInfo')
|
|
9
|
+
body = response.body[:get_position_info_response]
|
|
10
|
+
doc = Nokogiri::XML(body[:track_meta_data])
|
|
11
|
+
|
|
12
|
+
{
|
|
13
|
+
title: doc.xpath('//dc:title').inner_text,
|
|
14
|
+
artist: doc.xpath('//dc:creator').inner_text,
|
|
15
|
+
album: doc.xpath('//upnp:album').inner_text,
|
|
16
|
+
playlist_position: body[:track],
|
|
17
|
+
track_duration: body[:track_duration],
|
|
18
|
+
current_position: body[:rel_time],
|
|
19
|
+
uri: body[:track_uri],
|
|
20
|
+
album_art: "http://#{self.ip}:#{PORT}#{doc.xpath('//upnp:albumArtURI').inner_text}"
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Pause the currently playing track.
|
|
25
|
+
def pause
|
|
26
|
+
send_transport_message('Pause')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Play the currently selected track or play a stream.
|
|
30
|
+
# @param [String] optional uri of the track to play. Leaving this blank, plays the current track.
|
|
31
|
+
def play(uri = nil)
|
|
32
|
+
# Play a song from the uri
|
|
33
|
+
set_av_transport_uri(uri) and return if uri
|
|
34
|
+
|
|
35
|
+
# Play the currently selected track
|
|
36
|
+
send_transport_message('Play')
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Stop playing.
|
|
40
|
+
def stop
|
|
41
|
+
send_transport_message('Stop')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Play the next track.
|
|
45
|
+
def next
|
|
46
|
+
send_transport_message('Next')
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Play the previous track.
|
|
50
|
+
def previous
|
|
51
|
+
send_transport_message('Previous')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Clear the queue
|
|
55
|
+
def clear_queue
|
|
56
|
+
send_transport_message('RemoveAllTracksFromQueue')
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Save queue
|
|
60
|
+
def save_queue(title)
|
|
61
|
+
name = 'SaveQueue'
|
|
62
|
+
action = "#{TRANSPORT_XMLNS}##{name}"
|
|
63
|
+
message = %Q{<u:#{name} xmlns:u="#{TRANSPORT_XMLNS}"><InstanceID>0</InstanceID><Title>#{title}</Title><ObjectID></ObjectID></u:#{name}>}
|
|
64
|
+
transport_client.call(name, soap_action: action, message: message)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Play a stream.
|
|
70
|
+
def set_av_transport_uri(uri)
|
|
71
|
+
name = 'SetAVTransportURI'
|
|
72
|
+
action = "#{TRANSPORT_XMLNS}##{name}"
|
|
73
|
+
message = %Q{<u:#{name} xmlns:u="#{TRANSPORT_XMLNS}"><InstanceID>0</InstanceID><CurrentURI>#{uri}</CurrentURI><CurrentURIMetaData></CurrentURIMetaData></u:#{name}>}
|
|
74
|
+
transport_client.call(name, soap_action: action, message: message)
|
|
75
|
+
self.play
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def transport_client
|
|
79
|
+
@transport_client ||= Savon.client endpoint: "http://#{self.ip}:#{Sonos::PORT}#{TRANSPORT_ENDPOINT}", namespace: Sonos::NAMESPACE
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def send_transport_message(name)
|
|
83
|
+
action = "#{TRANSPORT_XMLNS}##{name}"
|
|
84
|
+
message = %Q{<u:#{name} xmlns:u="#{TRANSPORT_XMLNS}"><InstanceID>0</InstanceID><Speed>1</Speed></u:#{name}>}
|
|
85
|
+
transport_client.call(name, soap_action: action, message: message)
|
|
86
|
+
end
|
|
87
|
+
end
|
data/lib/sonos/speaker.rb
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
require 'savon'
|
|
2
|
-
require '
|
|
3
|
-
require 'sonos/
|
|
4
|
-
require 'sonos/
|
|
5
|
-
require 'sonos/content_directory'
|
|
2
|
+
require 'open-uri'
|
|
3
|
+
require 'sonos/endpoint'
|
|
4
|
+
require 'sonos/topology'
|
|
6
5
|
|
|
7
6
|
module Sonos
|
|
8
7
|
class Speaker
|
|
9
|
-
include Transport
|
|
10
|
-
include Rendering
|
|
11
|
-
include Device
|
|
12
|
-
include ContentDirectory
|
|
8
|
+
include Endpoint::Transport
|
|
9
|
+
include Endpoint::Rendering
|
|
10
|
+
include Endpoint::Device
|
|
11
|
+
include Endpoint::ContentDirectory
|
|
12
|
+
include Topology
|
|
13
13
|
|
|
14
14
|
attr_reader :ip, :zone_name, :zone_icon, :uid, :serial_number, :software_version, :hardware_version, :mac_address
|
|
15
15
|
|
|
@@ -29,7 +29,7 @@ module Sonos
|
|
|
29
29
|
|
|
30
30
|
# Get information about the speaker.
|
|
31
31
|
def get_status
|
|
32
|
-
doc = Nokogiri::XML(open("http://#{@ip}
|
|
32
|
+
doc = Nokogiri::XML(open("http://#{@ip}:#{PORT}/status/zp"))
|
|
33
33
|
|
|
34
34
|
@zone_name = doc.xpath('.//ZoneName').inner_text
|
|
35
35
|
@zone_icon = doc.xpath('.//ZoneIcon').inner_text
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require 'uri'
|
|
2
|
+
|
|
3
|
+
module Sonos
|
|
4
|
+
module Topology
|
|
5
|
+
def topology
|
|
6
|
+
doc = Nokogiri::XML(open("http://#{@ip}:#{PORT}/status/topology"))
|
|
7
|
+
|
|
8
|
+
doc.xpath('//ZonePlayers/ZonePlayer').map do |node|
|
|
9
|
+
Node.new(node)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
protected
|
|
14
|
+
|
|
15
|
+
class Node
|
|
16
|
+
attr_accessor :name, :group, :coordinator, :location, :version, :uuid
|
|
17
|
+
|
|
18
|
+
def initialize(node)
|
|
19
|
+
node.attributes.each do |k, v|
|
|
20
|
+
self.send("#{k}=", v) if self.respond_to?(k.to_sym)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
self.name = node.inner_text
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def ip
|
|
27
|
+
@ip ||= URI.parse(location).host
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def speaker
|
|
31
|
+
@speaker || Sonos::Speaker.new(ip)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/sonos/version.rb
CHANGED
data/sonos.gemspec
CHANGED
|
@@ -6,8 +6,8 @@ require 'sonos/version'
|
|
|
6
6
|
Gem::Specification.new do |gem|
|
|
7
7
|
gem.name = 'sonos'
|
|
8
8
|
gem.version = Sonos::VERSION
|
|
9
|
-
gem.authors = ['Sam Soffes']
|
|
10
|
-
gem.email = ['sam@soff.es']
|
|
9
|
+
gem.authors = ['Sam Soffes', 'Aaron Gotwalt']
|
|
10
|
+
gem.email = ['sam@soff.es', 'gotwalt@gmail.com']
|
|
11
11
|
gem.description = 'Control Sonos speakers with Ruby'
|
|
12
12
|
gem.summary = gem.description
|
|
13
13
|
gem.homepage = 'https://github.com/soffes/sonos'
|
|
@@ -18,4 +18,5 @@ Gem::Specification.new do |gem|
|
|
|
18
18
|
gem.require_paths = ['lib']
|
|
19
19
|
|
|
20
20
|
gem.add_dependency 'savon', '~> 2.0.2'
|
|
21
|
+
gem.add_dependency 'thor'
|
|
21
22
|
end
|
metadata
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sonos
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
prerelease:
|
|
6
6
|
platform: ruby
|
|
7
7
|
authors:
|
|
8
8
|
- Sam Soffes
|
|
9
|
+
- Aaron Gotwalt
|
|
9
10
|
autorequire:
|
|
10
11
|
bindir: bin
|
|
11
12
|
cert_chain: []
|
|
@@ -27,10 +28,28 @@ dependencies:
|
|
|
27
28
|
- - ~>
|
|
28
29
|
- !ruby/object:Gem::Version
|
|
29
30
|
version: 2.0.2
|
|
31
|
+
- !ruby/object:Gem::Dependency
|
|
32
|
+
name: thor
|
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
|
34
|
+
none: false
|
|
35
|
+
requirements:
|
|
36
|
+
- - ! '>='
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '0'
|
|
39
|
+
type: :runtime
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
none: false
|
|
43
|
+
requirements:
|
|
44
|
+
- - ! '>='
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
30
47
|
description: Control Sonos speakers with Ruby
|
|
31
48
|
email:
|
|
32
49
|
- sam@soff.es
|
|
33
|
-
|
|
50
|
+
- gotwalt@gmail.com
|
|
51
|
+
executables:
|
|
52
|
+
- sonos
|
|
34
53
|
extensions: []
|
|
35
54
|
extra_rdoc_files: []
|
|
36
55
|
files:
|
|
@@ -40,12 +59,17 @@ files:
|
|
|
40
59
|
- LICENSE
|
|
41
60
|
- Rakefile
|
|
42
61
|
- Readme.markdown
|
|
62
|
+
- bin/sonos
|
|
43
63
|
- lib/sonos.rb
|
|
44
|
-
- lib/sonos/
|
|
45
|
-
- lib/sonos/
|
|
46
|
-
- lib/sonos/
|
|
64
|
+
- lib/sonos/cli.rb
|
|
65
|
+
- lib/sonos/discovery.rb
|
|
66
|
+
- lib/sonos/endpoint.rb
|
|
67
|
+
- lib/sonos/endpoint/content_directory.rb
|
|
68
|
+
- lib/sonos/endpoint/device.rb
|
|
69
|
+
- lib/sonos/endpoint/rendering.rb
|
|
70
|
+
- lib/sonos/endpoint/transport.rb
|
|
47
71
|
- lib/sonos/speaker.rb
|
|
48
|
-
- lib/sonos/
|
|
72
|
+
- lib/sonos/topology.rb
|
|
49
73
|
- lib/sonos/version.rb
|
|
50
74
|
- sonos.gemspec
|
|
51
75
|
homepage: https://github.com/soffes/sonos
|
|
@@ -62,7 +86,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
62
86
|
version: '0'
|
|
63
87
|
segments:
|
|
64
88
|
- 0
|
|
65
|
-
hash: -
|
|
89
|
+
hash: -1695773237927316888
|
|
66
90
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
67
91
|
none: false
|
|
68
92
|
requirements:
|
|
@@ -71,7 +95,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
71
95
|
version: '0'
|
|
72
96
|
segments:
|
|
73
97
|
- 0
|
|
74
|
-
hash: -
|
|
98
|
+
hash: -1695773237927316888
|
|
75
99
|
requirements: []
|
|
76
100
|
rubyforge_project:
|
|
77
101
|
rubygems_version: 1.8.23
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
module Sonos
|
|
2
|
-
module ContentDirectory
|
|
3
|
-
CONTENT_DIRECTORY_ENDPOINT = '/MediaServer/ContentDirectory/Control'
|
|
4
|
-
CONTENT_DIRECTORY_XMLNS = 'urn:schemas-upnp-org:service:ContentDirectory:1'
|
|
5
|
-
|
|
6
|
-
# Get the current queue
|
|
7
|
-
def queue(starting_index = 0, requested_count = 100)
|
|
8
|
-
name = 'Browse'
|
|
9
|
-
action = "#{CONTENT_DIRECTORY_XMLNS}##{name}"
|
|
10
|
-
message = %Q{<u:#{name} xmlns:u="#{CONTENT_DIRECTORY_XMLNS}"><ObjectID>Q:0</ObjectID><BrowseFlag>BrowseDirectChildren</BrowseFlag><Filter>dc:title,res,dc:creator,upnp:artist,upnp:album,upnp:albumArtURI</Filter><StartingIndex>#{starting_index}</StartingIndex><RequestedCount>#{requested_count}</RequestedCount><SortCriteria></SortCriteria></u:Browse>}
|
|
11
|
-
result = content_directory_client.call name, soap_action: action, message: message
|
|
12
|
-
body = result.body[:browse_response]
|
|
13
|
-
|
|
14
|
-
hash = {
|
|
15
|
-
total: body[:total_matches].to_i,
|
|
16
|
-
items: parse_items(body[:result])
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
# Paginate
|
|
20
|
-
# TODO: This is ugly and inflexible
|
|
21
|
-
if starting_index == 0
|
|
22
|
-
start = starting_index
|
|
23
|
-
while hash[:items].count < hash[:total]
|
|
24
|
-
start += requested_count
|
|
25
|
-
hash[:items] += browse(start, requested_count)[:items]
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
hash
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
private
|
|
33
|
-
|
|
34
|
-
def content_directory_client
|
|
35
|
-
@content_directory_client ||= Savon.client endpoint: "http://#{self.ip}:#{PORT}#{CONTENT_DIRECTORY_ENDPOINT}", namespace: NAMESPACE
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def parse_items(string)
|
|
39
|
-
result = []
|
|
40
|
-
doc = Nokogiri::XML(string)
|
|
41
|
-
doc.css('item').each do |item|
|
|
42
|
-
res = item.css('res').first
|
|
43
|
-
result << {
|
|
44
|
-
title: item.xpath('dc:title').inner_text,
|
|
45
|
-
artist: item.xpath('dc:creator').inner_text,
|
|
46
|
-
album: item.xpath('upnp:album').inner_text,
|
|
47
|
-
album_art: "http://#{self.ip}:#{PORT}#{item.xpath('upnp:albumArtURI').inner_text}",
|
|
48
|
-
duration: res['duration'],
|
|
49
|
-
id: res.inner_text
|
|
50
|
-
}
|
|
51
|
-
end
|
|
52
|
-
result
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
data/lib/sonos/device.rb
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
module Sonos
|
|
2
|
-
module Device
|
|
3
|
-
DEVICE_ENDPOINT = '/DeviceProperties/Control'
|
|
4
|
-
DEVICE_XMLNS = 'urn:schemas-upnp-org:service:DeviceProperties:1'
|
|
5
|
-
|
|
6
|
-
# Turn the white status light on or off
|
|
7
|
-
# @param [Boolean] True to turn on the light. False to turn off the light.
|
|
8
|
-
def status_light_enabled=(enabled)
|
|
9
|
-
send_device_message('SetLEDState', enabled ? 'On' : 'Off')
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
private
|
|
13
|
-
|
|
14
|
-
def device_client
|
|
15
|
-
@device_client ||= Savon.client endpoint: "http://#{self.ip}:#{PORT}#{DEVICE_ENDPOINT}", namespace: NAMESPACE
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def send_device_message(name, value)
|
|
19
|
-
action = "#{DEVICE_XMLNS}##{name}"
|
|
20
|
-
attribute = name.sub('Set', '')
|
|
21
|
-
message = %Q{<u:#{name} xmlns:u="#{DEVICE_XMLNS}"><Desired#{attribute}>#{value}</Desired#{attribute}>}
|
|
22
|
-
device_client.call(name, soap_action: action, message: message)
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
data/lib/sonos/rendering.rb
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
module Sonos
|
|
2
|
-
module Rendering
|
|
3
|
-
RENDERING_ENDPOINT = '/MediaRenderer/RenderingControl/Control'
|
|
4
|
-
RENDERING_XMLNS = 'urn:schemas-upnp-org:service:RenderingControl:1'
|
|
5
|
-
|
|
6
|
-
# Get the current volume.
|
|
7
|
-
# @return [Fixnum] the volume from 0 to 100
|
|
8
|
-
def volume
|
|
9
|
-
response = send_rendering_message('GetVolume')
|
|
10
|
-
response.body[:get_volume_response][:current_volume].to_i
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
# Set the volume from 0 to 100.
|
|
14
|
-
# @param [Fixnum] the desired volume from 0 to 100
|
|
15
|
-
def volume=(value)
|
|
16
|
-
send_rendering_message('SetVolume', value)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
# Get the current bass EQ.
|
|
20
|
-
# @return [Fixnum] the base EQ from -10 to 10
|
|
21
|
-
def bass
|
|
22
|
-
response = send_rendering_message('GetBass')
|
|
23
|
-
response.body[:get_bass_response][:current_bass].to_i
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Set the bass EQ from -10 to 10.
|
|
27
|
-
# @param [Fixnum] the desired bass EQ from -10 to 10
|
|
28
|
-
def bass=(value)
|
|
29
|
-
send_rendering_message('SetBass', value)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Get the current treble EQ.
|
|
33
|
-
# @return [Fixnum] the treble EQ from -10 to 10
|
|
34
|
-
def treble
|
|
35
|
-
response = send_rendering_message('GetTreble')
|
|
36
|
-
response.body[:get_treble_response][:current_treble].to_i
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Set the treble EQ from -10 to 10.
|
|
40
|
-
# @param [Fixnum] the desired treble EQ from -10 to 10
|
|
41
|
-
def treble=(value)
|
|
42
|
-
send_rendering_message('SetTreble', value)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Mute the speaker
|
|
46
|
-
def mute
|
|
47
|
-
set_mute(true)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Unmute the speaker
|
|
51
|
-
def unmute
|
|
52
|
-
set_mute(false)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Is the speaker muted?
|
|
56
|
-
# @return [Boolean] true if the speaker is muted and false if it is not
|
|
57
|
-
def muted?
|
|
58
|
-
response = send_rendering_message('GetMute')
|
|
59
|
-
response.body[:get_mute_response][:current_mute] == '1'
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
private
|
|
63
|
-
|
|
64
|
-
# Sets the speaker's mute
|
|
65
|
-
# @param [Boolean] if the speaker is muted or not
|
|
66
|
-
def set_mute(value)
|
|
67
|
-
send_rendering_message('SetMute', value ? 1 : 0)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def rendering_client
|
|
71
|
-
@rendering_client ||= Savon.client endpoint: "http://#{self.ip}:#{PORT}#{RENDERING_ENDPOINT}", namespace: NAMESPACE
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def send_rendering_message(name, value = nil)
|
|
75
|
-
action = "#{RENDERING_XMLNS}##{name}"
|
|
76
|
-
message = %Q{<u:#{name} xmlns:u="#{RENDERING_XMLNS}"><InstanceID>0</InstanceID><Channel>Master</Channel>}
|
|
77
|
-
|
|
78
|
-
if value
|
|
79
|
-
attribute = name.sub('Set', '')
|
|
80
|
-
message += %Q{<Desired#{attribute}>#{value}</Desired#{attribute}></u:#{name}>}
|
|
81
|
-
else
|
|
82
|
-
message += %Q{</u:#{name}>}
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
rendering_client.call(name, soap_action: action, message: message)
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
data/lib/sonos/transport.rb
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
module Sonos
|
|
2
|
-
module Transport
|
|
3
|
-
TRANSPORT_ENDPOINT = '/MediaRenderer/AVTransport/Control'
|
|
4
|
-
TRANSPORT_XMLNS = 'urn:schemas-upnp-org:service:AVTransport:1'
|
|
5
|
-
|
|
6
|
-
# Get information about the currently playing track.
|
|
7
|
-
# @return [Hash] information about the current track.
|
|
8
|
-
def now_playing
|
|
9
|
-
response = send_transport_message('GetPositionInfo')
|
|
10
|
-
body = response.body[:get_position_info_response]
|
|
11
|
-
doc = Nokogiri::XML(body[:track_meta_data])
|
|
12
|
-
|
|
13
|
-
{
|
|
14
|
-
title: doc.xpath('//dc:title').inner_text,
|
|
15
|
-
artist: doc.xpath('//dc:creator').inner_text,
|
|
16
|
-
album: doc.xpath('//upnp:album').inner_text,
|
|
17
|
-
playlist_position: body[:track],
|
|
18
|
-
track_duration: body[:track_duration],
|
|
19
|
-
current_position: body[:rel_time],
|
|
20
|
-
uri: body[:track_uri],
|
|
21
|
-
album_art: "http://#{self.ip}:#{PORT}#{doc.xpath('//upnp:albumArtURI').inner_text}"
|
|
22
|
-
}
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Pause the currently playing track.
|
|
26
|
-
def pause
|
|
27
|
-
send_transport_message('Pause')
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Play the currently selected track or play a stream.
|
|
31
|
-
# @param [String] optional uri of the track to play. Leaving this blank, plays the current track.
|
|
32
|
-
def play(uri = nil)
|
|
33
|
-
# Play a song from the uri
|
|
34
|
-
set_av_transport_uri(uri) and return if uri
|
|
35
|
-
|
|
36
|
-
# Play the currently selected track
|
|
37
|
-
send_transport_message('Play')
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Stop playing.
|
|
41
|
-
def stop
|
|
42
|
-
send_transport_message('Stop')
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Play the next track.
|
|
46
|
-
def next
|
|
47
|
-
send_transport_message('Next')
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Play the previous track.
|
|
51
|
-
def previous
|
|
52
|
-
send_transport_message('Previous')
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Clear the queue
|
|
56
|
-
def clear_queue
|
|
57
|
-
send_transport_message('RemoveAllTracksFromQueue')
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# Save queue
|
|
61
|
-
def save_queue(title)
|
|
62
|
-
name = 'SaveQueue'
|
|
63
|
-
action = "#{TRANSPORT_XMLNS}##{name}"
|
|
64
|
-
message = %Q{<u:#{name} xmlns:u="#{TRANSPORT_XMLNS}"><InstanceID>0</InstanceID><Title>#{title}</Title><ObjectID></ObjectID></u:#{name}>}
|
|
65
|
-
transport_client.call(name, soap_action: action, message: message)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
private
|
|
69
|
-
|
|
70
|
-
# Play a stream.
|
|
71
|
-
def set_av_transport_uri(uri)
|
|
72
|
-
name = 'SetAVTransportURI'
|
|
73
|
-
action = "#{TRANSPORT_XMLNS}##{name}"
|
|
74
|
-
message = %Q{<u:#{name} xmlns:u="#{TRANSPORT_XMLNS}"><InstanceID>0</InstanceID><CurrentURI>#{uri}</CurrentURI><CurrentURIMetaData></CurrentURIMetaData></u:#{name}>}
|
|
75
|
-
transport_client.call(name, soap_action: action, message: message)
|
|
76
|
-
self.play
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def transport_client
|
|
80
|
-
@transport_client ||= Savon.client endpoint: "http://#{self.ip}:#{PORT}#{TRANSPORT_ENDPOINT}", namespace: NAMESPACE
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def send_transport_message(name)
|
|
84
|
-
action = "#{TRANSPORT_XMLNS}##{name}"
|
|
85
|
-
message = %Q{<u:#{name} xmlns:u="#{TRANSPORT_XMLNS}"><InstanceID>0</InstanceID><Speed>1</Speed></u:#{name}>}
|
|
86
|
-
transport_client.call(name, soap_action: action, message: message)
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
end
|