sonos 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/soffes/sonos) [![Dependency Status](https://gemnasium.com/soffes/sonos.png)](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'
|