sonos 0.2.1 → 0.3.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/.travis.yml +4 -0
- data/Changelog.markdown +7 -0
- data/Gemfile +11 -1
- data/Rakefile +7 -0
- data/Readme.markdown +25 -13
- data/lib/sonos/cli.rb +21 -10
- data/lib/sonos/device/base.rb +79 -0
- data/lib/sonos/device/bridge.rb +17 -0
- data/lib/sonos/device/speaker.rb +25 -0
- data/lib/sonos/device.rb +8 -0
- data/lib/sonos/discovery.rb +18 -5
- data/lib/sonos/endpoint/{transport.rb → a_v_transport.rb} +34 -13
- data/lib/sonos/endpoint/rendering.rb +2 -0
- data/lib/sonos/endpoint.rb +1 -1
- data/lib/sonos/group.rb +48 -0
- data/lib/sonos/system.rb +59 -0
- data/lib/sonos/topology_node.rb +21 -0
- data/lib/sonos/version.rb +1 -1
- data/lib/sonos.rb +12 -7
- data/sonos.gemspec +2 -0
- data/test/cassettes/topology.yml +840 -0
- data/test/support/discovery_macros.rb +7 -0
- data/test/support/vcr.rb +4 -0
- data/test/test_helper.rb +15 -0
- data/test/units/system_test.rb +21 -0
- metadata +26 -12
- data/lib/sonos/speaker.rb +0 -44
- data/lib/sonos/topology.rb +0 -35
data/.travis.yml
ADDED
data/Changelog.markdown
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
### Version 0.3.0 — February 2, 2012
|
2
|
+
|
3
|
+
* System owns groups that reflect the topology
|
4
|
+
* Group and ungroup speakers
|
5
|
+
* Rename `playlist_position` to `queue_position` for consistency
|
6
|
+
* Add `seek`
|
7
|
+
|
1
8
|
### Version 0.2.1 — December 24, 2012
|
2
9
|
|
3
10
|
* Fix album art in now playing
|
data/Gemfile
CHANGED
@@ -1,7 +1,17 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
2
|
|
3
|
-
#
|
3
|
+
# Gem dependencies
|
4
4
|
gemspec
|
5
5
|
|
6
|
+
# Development dependencies
|
6
7
|
gem 'rake'
|
7
8
|
gem 'yard'
|
9
|
+
|
10
|
+
# Testing dependencies
|
11
|
+
group :test do
|
12
|
+
gem 'minitest'
|
13
|
+
gem 'minitest-wscolor'
|
14
|
+
gem 'fakeweb'
|
15
|
+
gem 'vcr'
|
16
|
+
gem 'mocha', require: false
|
17
|
+
end
|
data/Rakefile
CHANGED
data/Readme.markdown
CHANGED
@@ -4,6 +4,8 @@ Control Sonos speakers with Ruby.
|
|
4
4
|
|
5
5
|
Huge thanks to [Rahim Sonawalla](https://github.com/rahims) for making [SoCo](https://github.com/rahims/SoCo). This gem would not be possible without his work.
|
6
6
|
|
7
|
+
[](https://codeclimate.com/github/soffes/sonos) [](https://gemnasium.com/soffes/sonos)
|
8
|
+
|
7
9
|
## Installation
|
8
10
|
|
9
11
|
Add this line to your application's Gemfile:
|
@@ -36,7 +38,8 @@ $ irb
|
|
36
38
|
``` ruby
|
37
39
|
require 'rubygems'
|
38
40
|
require 'sonos'
|
39
|
-
|
41
|
+
system = Sonos::System.new # Auto-discovers your system
|
42
|
+
speaker = system.speakers.first
|
40
43
|
```
|
41
44
|
|
42
45
|
Now that we have a reference to the speaker, we can do all kinds of stuff.
|
@@ -56,41 +59,48 @@ speaker.clear_queue
|
|
56
59
|
|
57
60
|
### Topology
|
58
61
|
|
59
|
-
`Sonos.discover` finds the first speaker it can. We can get all of the Sonos devices (including Bridges, etc) by calling `
|
62
|
+
`Sonos.discover` finds the first speaker it can. We can get all of the Sonos devices (including Bridges, etc) by calling `Sonos.system.devices`. To get the groups, call `Sonos.system.groups`.
|
63
|
+
|
64
|
+
All of this is based off of the raw `Sonos.system.topology`.
|
60
65
|
|
61
66
|
### CLI
|
62
67
|
|
63
|
-
There is a very limited CLI right now. You can run `sonos
|
68
|
+
There is a very limited CLI right now. You can run `sonos devices` to get the IP of all of your devices.
|
69
|
+
|
70
|
+
You can also run `sonos pause_all` to pause all your Sonos groups.
|
64
71
|
|
65
72
|
## To Do
|
66
73
|
|
67
74
|
### General
|
68
75
|
|
69
|
-
* Refactor all of the things
|
70
|
-
* Nonblocking calls with Celluloid::IO
|
71
|
-
* List other speakers
|
72
76
|
* Handle errors better
|
73
77
|
* Handle line-in in `now_playing`
|
74
|
-
*
|
78
|
+
* Detect fixed volume
|
79
|
+
* Detect stereo pair
|
75
80
|
* CLI client for everything
|
81
|
+
* Nonblocking calls with Celluloid::IO
|
76
82
|
|
77
83
|
### Features
|
78
84
|
|
79
|
-
*
|
85
|
+
* Manipulating groups doesn't update `System#groups`
|
80
86
|
* Pause all (there is no play all in the controller, we could loop through and do it though)
|
81
|
-
*
|
82
|
-
* Party Mode
|
83
|
-
* Join
|
87
|
+
* Party Mode
|
84
88
|
* Line-in
|
85
89
|
* Toggle cross fade
|
86
90
|
* Toggle shuffle
|
87
91
|
* Set repeat mode
|
88
|
-
* Scrub
|
89
92
|
* Search music library
|
90
93
|
* Browse music library
|
91
94
|
* Add songs to queue
|
92
95
|
* Skip to song in queue
|
96
|
+
* Alarm clock
|
93
97
|
* Sleep timer
|
98
|
+
* Pandora doesn't use the Queue. I bet things are all jacked up.
|
99
|
+
* CONNECT (and possibly PLAY:5) line in settings
|
100
|
+
* Source name
|
101
|
+
* Level
|
102
|
+
* Autoplay room
|
103
|
+
* Autoplay include grouped rooms
|
94
104
|
|
95
105
|
### Maybe
|
96
106
|
|
@@ -98,14 +108,16 @@ If we are implementing everything the official Sonos Controller does, here's som
|
|
98
108
|
|
99
109
|
* Set zone name and icon
|
100
110
|
* Create stero pair
|
101
|
-
* Support for
|
111
|
+
* Support for SUB
|
102
112
|
* Support for DOCK
|
113
|
+
* Support for CONNECT:AMP (not sure if this is any different from CONNECT)
|
103
114
|
* Manage services
|
104
115
|
* Date and time
|
105
116
|
* Wireless channel
|
106
117
|
* Audio compression
|
107
118
|
* Automatically check for updates (not sure if this is a controller only preference)
|
108
119
|
* Local music servers
|
120
|
+
* Add component
|
109
121
|
|
110
122
|
## Contributing
|
111
123
|
|
data/lib/sonos/cli.rb
CHANGED
@@ -3,18 +3,29 @@ require 'sonos'
|
|
3
3
|
|
4
4
|
module Sonos
|
5
5
|
class Cli < Thor
|
6
|
-
desc '
|
7
|
-
|
8
|
-
|
9
|
-
|
6
|
+
desc 'devices', 'Finds the IP address of all of the Sonos devices on your network'
|
7
|
+
def devices
|
8
|
+
system.devices.each do |device|
|
9
|
+
puts device.name.ljust(20) + device.ip
|
10
|
+
end
|
11
|
+
end
|
10
12
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
else
|
16
|
-
puts "#{speaker.zone_name.ljust(20)} #{speaker.ip}"
|
13
|
+
desc 'speakers', 'Finds the IP address of all of the Sonos speakers on your network'
|
14
|
+
def speakers
|
15
|
+
system.speakers.each do |speaker|
|
16
|
+
puts speaker.name.ljust(20) + speaker.ip
|
17
17
|
end
|
18
18
|
end
|
19
|
+
|
20
|
+
desc 'pause_all', 'Pauses all Sonos speaker groups'
|
21
|
+
def pause_all
|
22
|
+
system.pause_all
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def system
|
28
|
+
@system ||= Sonos::System.new
|
29
|
+
end
|
19
30
|
end
|
20
31
|
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
require 'nokogiri'
|
3
|
+
|
4
|
+
module Sonos::Device
|
5
|
+
class Base
|
6
|
+
attr_reader :ip, :name, :uid, :serial_number, :software_version, :hardware_version, :mac_address, :group
|
7
|
+
|
8
|
+
def self.detect(ip)
|
9
|
+
data = retrieve_information(ip)
|
10
|
+
model_number = data[:model_number]
|
11
|
+
|
12
|
+
if Bridge.model_numbers.include?(model_number)
|
13
|
+
Bridge.new(ip, data)
|
14
|
+
elsif Speaker.model_numbers.include?(model_number)
|
15
|
+
Speaker.new(ip, data)
|
16
|
+
else
|
17
|
+
raise ArgumentError.new("#{self.data[:model_number]} not supported")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(ip, data = nil)
|
22
|
+
@ip = ip
|
23
|
+
|
24
|
+
if data.nil?
|
25
|
+
self.data = Base.retrieve_information(ip)
|
26
|
+
else
|
27
|
+
self.data = data
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def data=(data)
|
32
|
+
@name = data[:name]
|
33
|
+
@uid = data[:uid]
|
34
|
+
@serial_number = data[:serial_number]
|
35
|
+
@software_version = data[:software_version]
|
36
|
+
@hardware_version = data[:hardware_version]
|
37
|
+
@zone_type = data[:zone_type]
|
38
|
+
@model_number = data[:model_number]
|
39
|
+
end
|
40
|
+
|
41
|
+
def data
|
42
|
+
{
|
43
|
+
name: @name,
|
44
|
+
uid: @uid,
|
45
|
+
serial_number: @serial_number,
|
46
|
+
software_version: @software_version,
|
47
|
+
hardware_version: @hardware_version,
|
48
|
+
zone_type: @zone_type,
|
49
|
+
model_number: @model_number
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
# Can this device play music?
|
54
|
+
# @return [Boolean] a boolean indicating if it can play music
|
55
|
+
def speaker?
|
56
|
+
false
|
57
|
+
end
|
58
|
+
|
59
|
+
protected
|
60
|
+
|
61
|
+
def self.retrieve_information(ip)
|
62
|
+
url = "http://#{ip}:#{Sonos::PORT}/xml/device_description.xml"
|
63
|
+
parse_description(Nokogiri::XML(open(url)))
|
64
|
+
end
|
65
|
+
|
66
|
+
# Get information about the device
|
67
|
+
def self.parse_description(doc)
|
68
|
+
{
|
69
|
+
name: doc.xpath('/xmlns:root/xmlns:device/xmlns:roomName').inner_text,
|
70
|
+
uid: doc.xpath('/xmlns:root/xmlns:device/xmlns:UDN').inner_text,
|
71
|
+
serial_number: doc.xpath('/xmlns:root/xmlns:device/xmlns:serialNum').inner_text,
|
72
|
+
software_version: doc.xpath('/xmlns:root/xmlns:device/xmlns:hardwareVersion').inner_text,
|
73
|
+
hardware_version: doc.xpath('/xmlns:root/xmlns:device/xmlns:softwareVersion').inner_text,
|
74
|
+
zone_type: doc.xpath('/xmlns:root/xmlns:device/xmlns:zoneType').inner_text,
|
75
|
+
model_number: doc.xpath('/xmlns:root/xmlns:device/xmlns:modelNumber').inner_text
|
76
|
+
}
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'savon'
|
2
|
+
require 'sonos/endpoint'
|
3
|
+
|
4
|
+
module Sonos::Device
|
5
|
+
|
6
|
+
# Used for PLAY:3, PLAY:5, and CONNECT
|
7
|
+
class Speaker < Base
|
8
|
+
include Sonos::Endpoint::AVTransport
|
9
|
+
include Sonos::Endpoint::Rendering
|
10
|
+
include Sonos::Endpoint::Device
|
11
|
+
include Sonos::Endpoint::ContentDirectory
|
12
|
+
|
13
|
+
MODEL_NUMBERS = ['S3', 'S5', 'ZP90']
|
14
|
+
|
15
|
+
attr_reader :icon
|
16
|
+
|
17
|
+
def self.model_numbers
|
18
|
+
MODEL_NUMBERS
|
19
|
+
end
|
20
|
+
|
21
|
+
def speaker?
|
22
|
+
true
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/sonos/device.rb
ADDED
data/lib/sonos/discovery.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'socket'
|
2
2
|
require 'ipaddr'
|
3
3
|
require 'timeout'
|
4
|
+
require 'sonos/topology_node'
|
4
5
|
|
5
6
|
#
|
6
7
|
# Inspired by https://github.com/rahims/SoCo, https://github.com/turboladen/upnp,
|
@@ -18,17 +19,29 @@ module Sonos
|
|
18
19
|
DEFAULT_TIMEOUT = 1
|
19
20
|
|
20
21
|
attr_reader :timeout
|
22
|
+
attr_reader :first_device_ip
|
21
23
|
|
22
|
-
def initialize(timeout =
|
23
|
-
@timeout = timeout
|
24
|
-
|
24
|
+
def initialize(timeout = DEFAULT_TIMEOUT)
|
25
|
+
@timeout = timeout
|
25
26
|
initialize_socket
|
26
27
|
end
|
27
28
|
|
29
|
+
# Look for Sonos devices on the network and return the first IP address found
|
30
|
+
# @return [String] the IP address of the first Sonos device found
|
28
31
|
def discover
|
29
32
|
send_discovery_message
|
30
|
-
|
31
|
-
|
33
|
+
@first_device_ip = listen_for_responses
|
34
|
+
end
|
35
|
+
|
36
|
+
# Find all of the Sonos devices on the network
|
37
|
+
# @return [Array] an array of TopologyNode objects
|
38
|
+
def topology
|
39
|
+
self.discover unless @first_device_ip
|
40
|
+
|
41
|
+
doc = Nokogiri::XML(open("http://#{@first_device_ip}:#{Sonos::PORT}/status/topology"))
|
42
|
+
doc.xpath('//ZonePlayers/ZonePlayer').map do |node|
|
43
|
+
TopologyNode.new(node)
|
44
|
+
end
|
32
45
|
end
|
33
46
|
|
34
47
|
private
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'uri'
|
2
2
|
|
3
|
-
module Sonos::Endpoint::
|
3
|
+
module Sonos::Endpoint::AVTransport
|
4
4
|
TRANSPORT_ENDPOINT = '/MediaRenderer/AVTransport/Control'
|
5
5
|
TRANSPORT_XMLNS = 'urn:schemas-upnp-org:service:AVTransport:1'
|
6
6
|
|
@@ -20,7 +20,7 @@ module Sonos::Endpoint::Transport
|
|
20
20
|
title: doc.xpath('//dc:title').inner_text,
|
21
21
|
artist: doc.xpath('//dc:creator').inner_text,
|
22
22
|
album: doc.xpath('//upnp:album').inner_text,
|
23
|
-
|
23
|
+
queue_position: body[:track],
|
24
24
|
track_duration: body[:track_duration],
|
25
25
|
current_position: body[:rel_time],
|
26
26
|
uri: body[:track_uri],
|
@@ -58,6 +58,16 @@ module Sonos::Endpoint::Transport
|
|
58
58
|
send_transport_message('Previous')
|
59
59
|
end
|
60
60
|
|
61
|
+
# Seeks to a given timestamp in the current track
|
62
|
+
# @param [Fixnum] seconds
|
63
|
+
def seek(seconds = 0)
|
64
|
+
# Must be sent in the format of HH:MM:SS
|
65
|
+
timestamp = Time.at(seconds).utc.strftime('%H:%M:%S')
|
66
|
+
|
67
|
+
send_transport_message('Seek', "<Unit>REL_TIME</Unit><Target>#{timestamp}</Target>")
|
68
|
+
|
69
|
+
end
|
70
|
+
|
61
71
|
# Clear the queue
|
62
72
|
def clear_queue
|
63
73
|
send_transport_message('RemoveAllTracksFromQueue')
|
@@ -65,30 +75,41 @@ module Sonos::Endpoint::Transport
|
|
65
75
|
|
66
76
|
# Save queue
|
67
77
|
def save_queue(title)
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
78
|
+
send_transport_message('SaveQueue', "<Title>#{title}</Title><ObjectID></ObjectID>")
|
79
|
+
end
|
80
|
+
|
81
|
+
# Join another speaker's group.
|
82
|
+
# Trying to call this on a stereo pair slave will fail.
|
83
|
+
def join(master)
|
84
|
+
set_av_transport_uri('x-rincon:' + master.uid.sub('uuid:', ''))
|
85
|
+
end
|
86
|
+
|
87
|
+
# Add another speaker to this group.
|
88
|
+
# Trying to call this on a stereo pair slave will fail.
|
89
|
+
def group(slave)
|
90
|
+
slave.join(self)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Ungroup from its current group.
|
94
|
+
# Trying to call this on a stereo pair slave will fail.
|
95
|
+
def ungroup
|
96
|
+
send_transport_message('BecomeCoordinatorOfStandaloneGroup')
|
72
97
|
end
|
73
98
|
|
74
99
|
private
|
75
100
|
|
76
101
|
# Play a stream.
|
77
102
|
def set_av_transport_uri(uri)
|
78
|
-
|
79
|
-
action = "#{TRANSPORT_XMLNS}##{name}"
|
80
|
-
message = %Q{<u:#{name} xmlns:u="#{TRANSPORT_XMLNS}"><InstanceID>0</InstanceID><CurrentURI>#{uri}</CurrentURI><CurrentURIMetaData></CurrentURIMetaData></u:#{name}>}
|
81
|
-
transport_client.call(name, soap_action: action, message: message)
|
82
|
-
self.play
|
103
|
+
send_transport_message('SetAVTransportURI', "<CurrentURI>#{uri}</CurrentURI><CurrentURIMetaData></CurrentURIMetaData>")
|
83
104
|
end
|
84
105
|
|
85
106
|
def transport_client
|
86
107
|
@transport_client ||= Savon.client endpoint: "http://#{self.ip}:#{Sonos::PORT}#{TRANSPORT_ENDPOINT}", namespace: Sonos::NAMESPACE
|
87
108
|
end
|
88
109
|
|
89
|
-
def send_transport_message(name)
|
110
|
+
def send_transport_message(name, part = '<Speed>1</Speed>')
|
90
111
|
action = "#{TRANSPORT_XMLNS}##{name}"
|
91
|
-
message = %Q{<u:#{name} xmlns:u="#{TRANSPORT_XMLNS}"><InstanceID>0</InstanceID
|
112
|
+
message = %Q{<u:#{name} xmlns:u="#{TRANSPORT_XMLNS}"><InstanceID>0</InstanceID>#{part}</u:#{name}>}
|
92
113
|
transport_client.call(name, soap_action: action, message: message)
|
93
114
|
end
|
94
115
|
end
|
@@ -3,6 +3,7 @@ module Sonos::Endpoint::Rendering
|
|
3
3
|
RENDERING_XMLNS = 'urn:schemas-upnp-org:service:RenderingControl:1'
|
4
4
|
|
5
5
|
# Get the current volume.
|
6
|
+
# Fixed volume speakers will always return 100.
|
6
7
|
# @return [Fixnum] the volume from 0 to 100
|
7
8
|
def volume
|
8
9
|
response = send_rendering_message('GetVolume')
|
@@ -10,6 +11,7 @@ module Sonos::Endpoint::Rendering
|
|
10
11
|
end
|
11
12
|
|
12
13
|
# Set the volume from 0 to 100.
|
14
|
+
# Trying to set the volume of a fixed volume speaker will fail.
|
13
15
|
# @param [Fixnum] the desired volume from 0 to 100
|
14
16
|
def volume=(value)
|
15
17
|
send_rendering_message('SetVolume', value)
|
data/lib/sonos/endpoint.rb
CHANGED
data/lib/sonos/group.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
module Sonos
|
2
|
+
# Represents a Sonos group. A group can contain one or more speakers. All speakers in a group
|
3
|
+
# play the same music in sync.
|
4
|
+
class Group
|
5
|
+
# The master speaker in the group
|
6
|
+
attr_reader :master_speaker
|
7
|
+
|
8
|
+
# All other speakers in the group
|
9
|
+
attr_reader :slave_speakers
|
10
|
+
|
11
|
+
def initialize(master_speaker, slave_speakers)
|
12
|
+
@master_speaker = master_speaker
|
13
|
+
@slave_speakers = (slave_speakers or [])
|
14
|
+
end
|
15
|
+
|
16
|
+
# All of the speakers in the group
|
17
|
+
def speakers
|
18
|
+
[self.master_speaker] + self.slave_speakers
|
19
|
+
end
|
20
|
+
|
21
|
+
# Remove all speakers from the group
|
22
|
+
def disband
|
23
|
+
self.slave_speakers.each do |speaker|
|
24
|
+
speaker.ungroup
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Full group name
|
29
|
+
def name
|
30
|
+
self.speakers.collect(&:name).uniq.join(', ')
|
31
|
+
end
|
32
|
+
|
33
|
+
# Forward AVTransport methods to the master speaker
|
34
|
+
%w{now_playing pause stop next previous queue clear_queue}.each do |method|
|
35
|
+
define_method(method) do
|
36
|
+
self.master_speaker.send(method.to_sym)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def play(uri = nil)
|
41
|
+
self.master_speaker.play(uri)
|
42
|
+
end
|
43
|
+
|
44
|
+
def save_queue(name)
|
45
|
+
self.master_speaker.save_queue(name)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/sonos/system.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
module Sonos
|
2
|
+
# The Sonos system. The root object to manage the collection of groups and devices. This is
|
3
|
+
# intended to be a singleton accessed from `Sonos.system`.
|
4
|
+
class System
|
5
|
+
attr_reader :topology
|
6
|
+
attr_reader :groups
|
7
|
+
attr_reader :devices
|
8
|
+
|
9
|
+
# Initialize the system
|
10
|
+
# @param [Array] the system topology. If this is nil, it will autodiscover.
|
11
|
+
def initialize(topology = Discovery.new.topology)
|
12
|
+
@topology = topology
|
13
|
+
@groups = []
|
14
|
+
@devices = @topology.collect(&:device)
|
15
|
+
|
16
|
+
construct_groups
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns all speakers
|
20
|
+
def speakers
|
21
|
+
@devices.select(&:speaker?)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Pause all speakers
|
25
|
+
def pause_all
|
26
|
+
self.groups.each do |group|
|
27
|
+
group.pause
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def construct_groups
|
34
|
+
# Loop through all of the unique groups
|
35
|
+
@topology.collect(&:group).uniq.each do |group_uid|
|
36
|
+
master_uuid = group_uid.split(':').first
|
37
|
+
nodes = []
|
38
|
+
master = nil
|
39
|
+
|
40
|
+
@topology.each do |node|
|
41
|
+
# Select all of the nodes with this group uid
|
42
|
+
next unless node.group == group_uid
|
43
|
+
|
44
|
+
if node.uuid == master_uuid
|
45
|
+
master = node
|
46
|
+
else
|
47
|
+
nodes << node
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Skip this group if there are no nodes or master
|
52
|
+
next if nodes.empty? or master.nil?
|
53
|
+
|
54
|
+
# Add the group
|
55
|
+
@groups << Group.new(master.device, nodes.collect(&:device))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Sonos
|
2
|
+
class TopologyNode
|
3
|
+
attr_accessor :name, :group, :coordinator, :location, :version, :uuid
|
4
|
+
|
5
|
+
def initialize(node)
|
6
|
+
node.attributes.each do |k, v|
|
7
|
+
self.send("#{k}=", v.inner_text) if self.respond_to?(k.to_sym)
|
8
|
+
end
|
9
|
+
|
10
|
+
self.name = node.inner_text
|
11
|
+
end
|
12
|
+
|
13
|
+
def ip
|
14
|
+
@ip ||= URI.parse(location).host
|
15
|
+
end
|
16
|
+
|
17
|
+
def device
|
18
|
+
@device || Device::Base.detect(ip)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/sonos/version.rb
CHANGED
data/lib/sonos.rb
CHANGED
@@ -1,16 +1,21 @@
|
|
1
1
|
require 'sonos/version'
|
2
|
-
require 'sonos/
|
2
|
+
require 'sonos/system'
|
3
3
|
require 'sonos/discovery'
|
4
|
+
require 'sonos/device'
|
5
|
+
require 'sonos/group'
|
4
6
|
|
5
7
|
module Sonos
|
6
8
|
PORT = 1400
|
7
9
|
NAMESPACE = 'http://www.sonos.com/Services/1.1'
|
8
10
|
|
9
|
-
|
10
|
-
|
11
|
-
|
11
|
+
# # Create a new speaker with it's IP address
|
12
|
+
# # @param [String] the speaker's IP address
|
13
|
+
# def self.speaker(ip)
|
14
|
+
# Device::Speaker.new(ip)
|
15
|
+
# end
|
12
16
|
|
13
|
-
|
14
|
-
|
15
|
-
|
17
|
+
# # Get the Sonos system
|
18
|
+
# def self.system
|
19
|
+
# @system ||= Sonos::System.new
|
20
|
+
# end
|
16
21
|
end
|
data/sonos.gemspec
CHANGED
@@ -11,12 +11,14 @@ Gem::Specification.new do |gem|
|
|
11
11
|
gem.description = 'Control Sonos speakers with Ruby'
|
12
12
|
gem.summary = gem.description
|
13
13
|
gem.homepage = 'https://github.com/soffes/sonos'
|
14
|
+
gem.license = 'MIT'
|
14
15
|
|
15
16
|
gem.files = `git ls-files`.split($/)
|
16
17
|
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
18
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
19
|
gem.require_paths = ['lib']
|
19
20
|
|
21
|
+
gem.required_ruby_version = '>= 1.9.2'
|
20
22
|
gem.add_dependency 'savon', '~> 2.0.2'
|
21
23
|
gem.add_dependency 'nokogiri'
|
22
24
|
gem.add_dependency 'thor'
|