Permalink
Browse files

System now owns groups that correctly represent the topology

  • Loading branch information...
1 parent 2c311cb commit 4f29e13063bcf2d9ea5cd91f335df0004198d5a0 @soffes soffes committed Dec 27, 2012
View
@@ -1,6 +1,8 @@
-### Version 0.2.2 — Unreleased
+### Version 0.3.0 — Unreleased
+* System owns groups that reflect the topology
* Group and ungroup speakers
+* Rename `playlist_position` to `queue_position` for consistency
### Version 0.2.1 — December 24, 2012
View
@@ -36,7 +36,7 @@ $ irb
``` ruby
require 'rubygems'
require 'sonos'
-speaker = Sonos.discover
+speaker = Sonos.system.speakers.first
```
Now that we have a reference to the speaker, we can do all kinds of stuff.
@@ -56,7 +56,9 @@ speaker.clear_queue
### Topology
-`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.
+`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`.
+
+All of this is based off of the raw `Sonos.system.topology`.
### CLI
@@ -71,7 +73,8 @@ There is a very limited CLI right now. You can run `sonos discover` to get the I
* List other speakers
* Handle errors better
* Handle line-in in `now_playing`
-* Better support for stero pairs
+* Detect fixed volume
+* Detect stereo pair
* CLI client for everything
### Features
View
@@ -1,22 +1,21 @@
+require 'sonos/version'
+require 'sonos/system'
+require 'sonos/discovery'
+require 'sonos/device'
+require 'sonos/group'
+
module Sonos
PORT = 1400
NAMESPACE = 'http://www.sonos.com/Services/1.1'
- def self.Speaker(ip)
- Speaker.new(ip)
- end
-
- def self.discover
- Sonos::Discovery.new.discover
+ # Create a new speaker with it's IP address
+ # @param [String] the speaker's IP address
+ def self.speaker(ip)
+ Device::Speaker.new(ip)
end
- module Device
+ # Get the Sonos system
+ def self.system
+ @system ||= Sonos::System.new
end
end
-
-require 'sonos/version'
-require 'sonos/discovery'
-
-require 'sonos/device/base'
-require 'sonos/device/speaker'
-require 'sonos/device/bridge'
View
@@ -0,0 +1,8 @@
+module Sonos
+ module Device
+ end
+end
+
+require 'sonos/device/base'
+require 'sonos/device/speaker'
+require 'sonos/device/bridge'
@@ -5,6 +5,19 @@ module Sonos::Device
class Base
attr_reader :ip, :name, :uid, :serial_number, :software_version, :hardware_version, :mac_address, :group
+ def self.detect(ip)
+ data = retrieve_information(ip)
+ model_number = data[:model_number]
+
+ if Bridge.model_numbers.include?(model_number)
+ Bridge.new(ip, data)
+ elsif Speaker.model_numbers.include?(model_number)
+ Speaker.new(ip, data)
+ else
+ raise ArgumentError.new("#{self.data[:model_number]} not supported")
+ end
+ end
+
def initialize(ip, data = nil)
@ip = ip
@@ -37,17 +50,10 @@ def data
}
end
- def self.detect(ip)
- data = retrieve_information(ip)
- model_number = data[:model_number]
-
- if Bridge.model_numbers.include?(model_number)
- Bridge.new(ip, data)
- elsif Speaker.model_numbers.include?(model_number)
- Speaker.new(ip, data)
- else
- raise ArgumentError.new("#{self.data[:model_number]} not supported")
- end
+ # Can this device play music?
+ # @return [Boolean] a boolean indicating if it can play music
+ def speaker?
+ false
end
protected
@@ -17,5 +17,9 @@ class Speaker < Base
def self.model_numbers
MODEL_NUMBERS
end
+
+ def speaker?
+ true
+ end
end
end
@@ -1,6 +1,7 @@
require 'socket'
require 'ipaddr'
require 'timeout'
+require 'sonos/topology_node'
#
# Inspired by https://github.com/rahims/SoCo, https://github.com/turboladen/upnp,
@@ -18,29 +19,33 @@ class Discovery
DEFAULT_TIMEOUT = 1
attr_reader :timeout
+ attr_reader :first_device_ip
- def initialize(timeout = nil)
- @timeout = timeout || DEFAULT_TIMEOUT
-
+ def initialize(timeout = DEFAULT_TIMEOUT)
+ @timeout = timeout
initialize_socket
end
+ # Look for Sonos devices on the network and return the first IP address found
+ # @return [String] the IP address of the first Sonos device found
def discover
send_discovery_message
- first_device_ip = listen_for_responses
- discover_topology(first_device_ip)
+ @first_device_ip = listen_for_responses
end
- private
-
- def discover_topology(ip_address)
- doc = Nokogiri::XML(open("http://#{ip_address}:#{Sonos::PORT}/status/topology"))
+ # Find all of the Sonos devices on the network
+ # @return [Array] an array of TopologyNode objects
+ def topology
+ self.discover unless @first_device_ip
+ doc = Nokogiri::XML(open("http://#{@first_device_ip}:#{Sonos::PORT}/status/topology"))
doc.xpath('//ZonePlayers/ZonePlayer').map do |node|
- TopologyNode.new(node).device
+ TopologyNode.new(node)
end
end
+ private
+
def send_discovery_message
# Request announcements
@socket.send(search_message, 0, MULTICAST_ADDR, MULTICAST_PORT)
@@ -77,25 +82,5 @@ def search_message
"ST: urn:schemas-upnp-org:device:ZonePlayer:1"
].join("\n")
end
-
- class TopologyNode
- attr_accessor :name, :group, :coordinator, :location, :version, :uuid
-
- def initialize(node)
- node.attributes.each do |k, v|
- self.send("#{k}=", v) if self.respond_to?(k.to_sym)
- end
-
- self.name = node.inner_text
- end
-
- def ip
- @ip ||= URI.parse(location).host
- end
-
- def device
- @device || Sonos::Device::Base.detect(ip)
- end
- end
end
end
@@ -20,7 +20,7 @@ def now_playing
title: doc.xpath('//dc:title').inner_text,
artist: doc.xpath('//dc:creator').inner_text,
album: doc.xpath('//upnp:album').inner_text,
- playlist_position: body[:track],
+ queue_position: body[:track],
track_duration: body[:track_duration],
current_position: body[:rel_time],
uri: body[:track_uri],
@@ -71,17 +71,20 @@ def save_queue(title)
transport_client.call(name, soap_action: action, message: message)
end
- # Join another speaker's group
+ # Join another speaker's group.
+ # Trying to call this on a stereo pair slave will fail.
def join(master)
set_av_transport_uri('x-rincon:' + master.uid.sub('uuid:', ''))
end
- # Add another speaker to this group
+ # Add another speaker to this group.
+ # Trying to call this on a stereo pair slave will fail.
def group(slave)
slave.join(self)
end
- # Ungroup from its current group
+ # Ungroup from its current group.
+ # Trying to call this on a stereo pair slave will fail.
def ungroup
send_transport_message('BecomeCoordinatorOfStandaloneGroup')
end
@@ -3,13 +3,15 @@ module Sonos::Endpoint::Rendering
RENDERING_XMLNS = 'urn:schemas-upnp-org:service:RenderingControl:1'
# Get the current volume.
+ # Fixed volume speakers will always return 100.
# @return [Fixnum] the volume from 0 to 100
def volume
response = send_rendering_message('GetVolume')
response.body[:get_volume_response][:current_volume].to_i
end
# Set the volume from 0 to 100.
+ # Trying to set the volume of a fixed volume speaker will fail.
# @param [Fixnum] the desired volume from 0 to 100
def volume=(value)
send_rendering_message('SetVolume', value)
View
@@ -1,5 +1,48 @@
module Sonos
+ # Represents a Sonos group. A group can contain one or more speakers. All speakers in a group
+ # play the same music in sync.
class Group
- attr_reader :devices
+ # The master speaker in the group
+ attr_reader :master_speaker
+
+ # All other speakers in the group
+ attr_reader :slave_speakers
+
+ def initialize(master_speaker, slave_speakers)
+ @master_speaker = master_speaker
+ @slave_speakers = (slave_speakers or [])
+ end
+
+ # All of the speakers in the group
+ def speakers
+ [self.master_speaker] + self.slave_speakers
+ end
+
+ # Remove all speakers from the group
+ def disband
+ self.slave_speakers.each do |speaker|
+ speaker.ungroup
+ end
+ end
+
+ # Full group name
+ def name
+ self.speakers.collect(&:name).uniq.join(', ')
+ end
+
+ # Forward AVTransport methods to the master speaker
+ %w{now_playing pause stop next previous queue clear_queue}.each do |method|
+ define_method(method) do
+ self.master_speaker.send(method.to_sym)
+ end
+ end
+
+ def play(uri = nil)
+ self.master_speaker.play(uri)
+ end
+
+ def save_queue(name)
+ self.master_speaker.save_queue(name)
+ end
end
end
View
@@ -1,15 +1,60 @@
module Sonos
+ # The Sonos system. The root object to manage the collection of groups and devices. This is
+ # intended to be a singleton accessed from `Sonos.system`.
class System
+ attr_reader :topology
+ attr_reader :groups
+ attr_reader :devices
+
+ # Initialize the system
+ # @param [Array] the system topology. If this is nil, it will autodiscover.
+ def initialize(topology = Sonos::Discovery.new.topology)
+ @topology = topology
+ construct_groups
+ end
+
+ # Returns all speakers
+ def speakers
+ @devices.select(&:speaker?)
+ end
+
+ # Pause all speakers
def pause_all
- # It looks like Sonos is just telling all of the groups to pause instead of
- # having a message to actually pause all
self.groups.each do |group|
group.pause
end
end
- def self.discover
- Sonos::Discovery.new.discover
+ private
+
+ def construct_groups
+ # Reset
+ @groups = []
+ @devices = @topology.collect(&:device)
+
+ # Loop through all of the unique groups
+ @topology.collect(&:group).uniq.each do |group_uid|
+ # Select all of the nodes with this group uid
+ nodes = @topology.select do |node|
+ node.group == group_uid
+ end
+
+ next if nodes.empty?
+
+ # Find master node
+ master_uuid = group_uid.split(':').first
+ master = nodes.select do |node|
+ node.uuid == master_uuid
+ end
+
+ next unless master.count == 1
+ master = master.first
+
+ nodes.delete(master)
+
+ # Add the group
+ @groups << Group.new(master.device, nodes.collect(&:device))
+ end
end
end
end
Oops, something went wrong.

0 comments on commit 4f29e13

Please sign in to comment.