sonos 0.3.3 → 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: df9944fdb181f7e979b67372e6457be32ddb7995
4
- data.tar.gz: b865559508e5b6ddf95a92b3d34a8935a659af3e
3
+ metadata.gz: f3ea780abc6b1575275463a67745acf4283db680
4
+ data.tar.gz: 7be54b8d1474a567b8e069f95b46ddd7b168b928
5
5
  SHA512:
6
- metadata.gz: 23639b6d07de2b63b13ac2b7a4f40be1a22ffc421595cc781dcdcdb4150ec0d00f95586d48b8a13fed8f4929d864665e3d015be8b912d1921be788b31fa35028
7
- data.tar.gz: ffc6f16907f7d7c2e010f193c5254cf4e7228cff3e37bb938f15981816ab6c88d50438df9c6686be87bab32bbe2c116991294e589112f1fcca9ec565c5799ae6
6
+ metadata.gz: 4c31701f021123f74e93189c7348e5a79ce774384fd2c87613f919fcd8b9a3387c5fe1eaaeeeb3c0e3e3f6c33f5fdb09462fb3e9046cbb20ca3b6fe0adf2460f
7
+ data.tar.gz: 92b8c154f0c759f54e4d24fb51580668bad8b727244c8b51f1594b3f733081fec13d6d3de43592bd0058f3c3ad7826c181b7783f5d95355a62ab2e4d1902aa6e
data/.gitignore CHANGED
@@ -3,6 +3,7 @@
3
3
  .bundle
4
4
  .config
5
5
  .yardoc
6
+ .idea
6
7
  Gemfile.lock
7
8
  InstalledFiles
8
9
  _yardoc
@@ -15,3 +16,4 @@ spec/reports
15
16
  test/tmp
16
17
  test/version_tmp
17
18
  tmp
19
+
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2012 Sam Soffes
1
+ Copyright (c) 2012-2013 Sam Soffes, http://soff.es
2
2
 
3
3
  MIT License
4
4
 
@@ -19,4 +19,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
19
  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
20
  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
21
  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile CHANGED
@@ -5,4 +5,14 @@ Rake::TestTask.new(:test) do |t|
5
5
  t.libs << 'test'
6
6
  t.pattern = 'test/**/*_test.rb'
7
7
  end
8
+
9
+ desc "Open an irb session preloaded with this API"
10
+ task :console do
11
+ $:.unshift(File.expand_path('../lib', __FILE__))
12
+ require 'sonos'
13
+ require 'irb'
14
+ ARGV.clear
15
+ IRB.start
16
+ end
17
+
8
18
  task default: :test
@@ -1,10 +1,10 @@
1
1
  # Sonos
2
2
 
3
- Control Sonos speakers with Ruby.
3
+ Control [Sonos](https://refer.sonos.com/raf/invite?code=k8k8mq9hg) 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)
7
+ [![Code Climate](https://codeclimate.com/github/soffes/sonos.png)](https://codeclimate.com/github/soffes/sonos)
8
8
 
9
9
  ## Installation
10
10
 
@@ -58,6 +58,36 @@ module Sonos
58
58
  speakers.each { |s| s.send command, *args }
59
59
  end
60
60
 
61
+ desc 'list_alarms', 'List all alarms.'
62
+ def list_alarms
63
+ puts speakers[0].list_alarms
64
+ end
65
+
66
+ desc 'is_alarm_enabled? [alarm_id]', 'Check if an alarm is enabled.'
67
+ def is_alarm_enabled?(alarm_id)
68
+ puts speakers[0].is_alarm_enabled?(alarm_id)
69
+ end
70
+
71
+ desc 'enable_alarm [alarm_id]', 'Enable an alarm.'
72
+ def enable_alarm(alarm_id)
73
+ speakers[0].enable_alarm(alarm_id)
74
+ end
75
+
76
+ desc 'disable_alarm [alarm_id]', 'Disable an alarm.'
77
+ def disable_alarm(alarm_id)
78
+ speakers[0].disable_alarm(alarm_id)
79
+ end
80
+
81
+ desc 'set_alarm_volume [alarm_id] [volume]', 'Set the volume for an alarm.'
82
+ def set_alarm_volume(alarm_id, volume)
83
+ speakers[0].set_alarm_volume(alarm_id, volume)
84
+ end
85
+
86
+ desc 'destroy_alarm [alarm_id]', 'Destroy an alarm.'
87
+ def destroy_alarm(alarm_id)
88
+ speakers[0].destroy_alarm(alarm_id)
89
+ end
90
+
61
91
  private
62
92
 
63
93
  def system
@@ -5,4 +5,4 @@ end
5
5
 
6
6
  require 'sonos/device/base'
7
7
  require 'sonos/device/speaker'
8
- require 'sonos/device/bridge'
8
+ require 'sonos/device/accessory'
@@ -0,0 +1,20 @@
1
+ require 'savon'
2
+ require 'sonos/endpoint'
3
+
4
+ module Sonos::Device
5
+
6
+ # Used for non-speaker Sonos devices
7
+ class Accessory < Base
8
+
9
+ MODELS = {
10
+ :'CR100' => 'CR100', # Released Jan 2005
11
+ :'CR200' => 'CONTROL', # Released Jul 2009
12
+ :'WD100' => 'DOCK',
13
+ :'ZB100' => 'BRIDGE' # Released Oct 2007
14
+ }.freeze
15
+
16
+ def self.models
17
+ MODELS
18
+ end
19
+ end
20
+ end
@@ -3,15 +3,19 @@ require 'nokogiri'
3
3
 
4
4
  module Sonos::Device
5
5
  class Base
6
- attr_reader :ip, :name, :uid, :serial_number, :software_version, :hardware_version, :mac_address, :group
6
+ attr_reader :ip, :name, :uid, :serial_number, :software_version, :hardware_version,
7
+ :zone_type, :model_number, :mac_address, :group, :icon
8
+
9
+ attr_accessor :group_master
7
10
 
8
11
  def self.detect(ip)
9
12
  data = retrieve_information(ip)
10
13
  model_number = data[:model_number]
11
14
 
12
- if Bridge.model_numbers.include?(model_number)
13
- Bridge.new(ip, data)
14
- elsif Speaker.model_numbers.include?(model_number)
15
+ # TODO: Clean up
16
+ if Accessory.models.keys.include?(model_number.to_sym)
17
+ Accessory.new(ip, data)
18
+ elsif Speaker.models.keys.include?(model_number.to_sym)
15
19
  Speaker.new(ip, data)
16
20
  else
17
21
  raise ArgumentError.new("#{data[:model_number]} not supported")
@@ -50,6 +54,12 @@ module Sonos::Device
50
54
  }
51
55
  end
52
56
 
57
+ # Get the device's model
58
+ # @return [String] a string representation of the device's model
59
+ def model
60
+ self.class.models[@model_number.to_sym]
61
+ end
62
+
53
63
  # Can this device play music?
54
64
  # @return [Boolean] a boolean indicating if it can play music
55
65
  def speaker?
@@ -3,23 +3,94 @@ require 'sonos/endpoint'
3
3
 
4
4
  module Sonos::Device
5
5
 
6
- # Used for PLAY:3, PLAY:5, and CONNECT
6
+ # Used for PLAY:3, PLAY:5, PLAYBAR, SUB, CONNECT and CONNECT:AMP
7
7
  class Speaker < Base
8
8
  include Sonos::Endpoint::AVTransport
9
9
  include Sonos::Endpoint::Rendering
10
10
  include Sonos::Endpoint::Device
11
11
  include Sonos::Endpoint::ContentDirectory
12
+ include Sonos::Endpoint::Upnp
13
+ include Sonos::Endpoint::Alarm
12
14
 
13
- MODEL_NUMBERS = ['S3', 'S5', 'S9', 'ZP90', 'Sub']
15
+ MODELS = {
16
+ :'S1' => 'PLAY:1', # Released Oct 2013
17
+ :'S3' => 'PLAY:3', # Released Jul 2011
18
+ :'S5' => 'PLAY:5', # Released Nov 2009
19
+ :'S9' => 'PLAYBAR', # Released Feb 2013
20
+ :'Sub' => 'SUB', # Released May 2012
21
+ :'ZP80' => 'ZP80', # Released Apr 2006
22
+ :'ZP90' => 'CONNECT', # Released Aug 2008
23
+ :'ZP100' => 'ZP100', # Released Jan 2005
24
+ :'ZP120' => 'CONNECT:AMP' # Released Aug 2008
25
+ }.freeze
14
26
 
15
- attr_reader :icon
16
-
17
- def self.model_numbers
18
- MODEL_NUMBERS
27
+ def self.models
28
+ MODELS
19
29
  end
20
30
 
21
31
  def speaker?
22
32
  true
23
33
  end
34
+
35
+ def shuffle_on
36
+ shuffle_repeat_change("shuffle_on")
37
+ end
38
+
39
+ def shuffle_off
40
+ shuffle_repeat_change("shuffle_off")
41
+ end
42
+
43
+ def repeat_on
44
+ shuffle_repeat_change("repeat_on")
45
+ end
46
+
47
+ def repeat_off
48
+ shuffle_repeat_change("repeat_off")
49
+ end
50
+
51
+ def crossfade_on
52
+ set_crossfade(true)
53
+ end
54
+
55
+ def crossfade_off
56
+ set_crossfade(false)
57
+ end
58
+
59
+ def shuffle_repeat_change(command)
60
+ status = get_playmode
61
+ case command
62
+ when "shuffle_on"
63
+ status[:shuffle] = true;
64
+ when "shuffle_off"
65
+ status[:shuffle] = false;
66
+ when "repeat_on"
67
+ status[:repeat] = true;
68
+ when "repeat_off"
69
+ status[:repeat] = false;
70
+ end
71
+ set_playmode(status)
72
+ end
73
+
74
+ def set_playmode(status = {:shuffle => false, :repeat => false})
75
+ send_transport_message('SetPlayMode', "<NewPlayMode>SHUFFLE</NewPlayMode>") if (status[:shuffle] && status[:repeat] )
76
+ send_transport_message('SetPlayMode', "<NewPlayMode>SHUFFLE_NOREPEAT</NewPlayMode>") if (status[:shuffle] && !status[:repeat])
77
+ send_transport_message('SetPlayMode', "<NewPlayMode>REPEAT_ALL</NewPlayMode>") if (!status[:shuffle] && status[:repeat] )
78
+ send_transport_message('SetPlayMode', "<NewPlayMode>NORMAL</NewPlayMode>") if (!status[:shuffle] && !status[:repeat])
79
+ end
80
+
81
+ def set_crossfade(crossfade)
82
+ crossfade_value = crossfade ? 1 : 0
83
+ send_transport_message('SetCrossfadeMode', "<InstanceID>0</InstanceID><CrossfadeMode>#{crossfade_value}</CrossfadeMode>")
84
+ end
85
+
86
+ def get_playmode
87
+ doc = Nokogiri::XML(open("http://#{self.group_master.ip}:#{Sonos::PORT}/status/playmode"))
88
+ playmode = {}
89
+ playmode[:shuffle] = doc.xpath('//Shuffle').inner_text == "On"
90
+ playmode[:repeat] = doc.xpath('//Repeat').inner_text == "On"
91
+ playmode[:crossfade] = doc.xpath('//Crossfade').inner_text == "On"
92
+ playmode
93
+ end
94
+
24
95
  end
25
96
  end
@@ -40,6 +40,7 @@ module Sonos
40
40
  # @return [Array] an array of TopologyNode objects
41
41
  def topology
42
42
  self.discover unless @first_device_ip
43
+ return [] unless @first_device_ip
43
44
 
44
45
  doc = Nokogiri::XML(open("http://#{@first_device_ip}:#{Sonos::PORT}/status/topology"))
45
46
  doc.xpath('//ZonePlayers/ZonePlayer').map do |node|
@@ -7,3 +7,5 @@ require 'sonos/endpoint/content_directory'
7
7
  require 'sonos/endpoint/device'
8
8
  require 'sonos/endpoint/rendering'
9
9
  require 'sonos/endpoint/a_v_transport'
10
+ require 'sonos/endpoint/upnp'
11
+ require 'sonos/endpoint/alarm'
@@ -71,9 +71,7 @@ module Sonos::Endpoint::AVTransport
71
71
  def seek(seconds = 0)
72
72
  # Must be sent in the format of HH:MM:SS
73
73
  timestamp = Time.at(seconds).utc.strftime('%H:%M:%S')
74
-
75
74
  send_transport_message('Seek', "<Unit>REL_TIME</Unit><Target>#{timestamp}</Target>")
76
-
77
75
  end
78
76
 
79
77
  # Clear the queue
@@ -130,7 +128,7 @@ module Sonos::Endpoint::AVTransport
130
128
  end
131
129
 
132
130
  def transport_client
133
- @transport_client ||= Savon.client endpoint: "http://#{self.ip}:#{Sonos::PORT}#{TRANSPORT_ENDPOINT}", namespace: Sonos::NAMESPACE
131
+ @transport_client ||= Savon.client endpoint: "http://#{self.group_master.ip}:#{Sonos::PORT}#{TRANSPORT_ENDPOINT}", namespace: Sonos::NAMESPACE, log_level: :error
134
132
  end
135
133
 
136
134
  def send_transport_message(name, part = '<Speed>1</Speed>')
@@ -0,0 +1,125 @@
1
+ # POST /AlarmClock/Control HTTP/1.1
2
+ # SOAPACTION: "urn:schemas-upnp-org:service:AlarmClock:1#ListAlarms"
3
+
4
+ # Request
5
+ #<?xml version="1.0" encoding="UTF-8"?>
6
+ #<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
7
+ # <s:Body>
8
+ # <u:ListAlarms xmlns:u="urn:schemas-upnp-org:service:AlarmClock:1" />
9
+ # </s:Body>
10
+ #</s:Envelope>
11
+
12
+ # Response
13
+ #<?xml version="1.0" encoding="UTF-8"?>
14
+ #<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
15
+ # <s:Body>
16
+ # <u:ListAlarmsResponse xmlns:u="urn:schemas-upnp-org:service:AlarmClock:1">
17
+ # <CurrentAlarmList>&lt;Alarms&gt;&lt;Alarm ID="8" StartTime="19:21:00" Duration="02:00:00" Recurrence="ONCE" Enabled="0" RoomUUID="RINCON_000E583564A601400" ProgramURI="x-rincon-buzzer:0" ProgramMetaData="" PlayMode="SHUFFLE_NOREPEAT" Volume="25" IncludeLinkedZones="0"/&gt;&lt;/Alarms&gt;</CurrentAlarmList>
18
+ # <CurrentAlarmListVersion>RINCON_000E583564A601400:26</CurrentAlarmListVersion>
19
+ # </u:ListAlarmsResponse>
20
+ # </s:Body>
21
+ #</s:Envelope>
22
+
23
+ module Sonos::Endpoint::Alarm
24
+ ALARM_CLOCK_ENDPOINT = '/AlarmClock/Control'
25
+ ALARM_CLOCK_XMLNS = 'urn:schemas-upnp-org:service:AlarmClock:1'
26
+
27
+ # List the alarms that have been defined
28
+ def list_alarms
29
+ hash_of_alarm_hashes = {}
30
+ response = send_alarm_message('ListAlarms')
31
+ alarm_list_reader = Nokogiri::XML::Reader(response.to_hash[:list_alarms_response][:current_alarm_list])
32
+ alarm_list_reader.each do |alarm_node|
33
+ id = alarm_node.attribute('ID')
34
+ unless id.nil?
35
+ alarm_hash = {
36
+ :ID => id,
37
+ # UpdateAlarm expects 'StartLocalTime', but the ListAlarm response attribute is 'StartTime'
38
+ :StartLocalTime => alarm_node.attribute('StartTime'),
39
+ :Duration => alarm_node.attribute('Duration'),
40
+ :Recurrence => alarm_node.attribute('Recurrence'),
41
+ :Enabled => alarm_node.attribute('Enabled'),
42
+ :RoomUUID => alarm_node.attribute('RoomUUID'),
43
+ :PlayMode => alarm_node.attribute('PlayMode'),
44
+ :Volume => alarm_node.attribute('Volume'),
45
+ :IncludeLinkedZones => alarm_node.attribute('IncludeLinkedZones'),
46
+ :ProgramURI => alarm_node.attribute('ProgramURI'),
47
+ :ProgramMetaData => alarm_node.attribute('ProgramMetaData')
48
+ }
49
+ hash_of_alarm_hashes[id] = alarm_hash
50
+ end
51
+ end
52
+ return hash_of_alarm_hashes
53
+ end
54
+
55
+ def create_alarm(startLocalTime, duration, recurrence, enabled, roomUuid, playMode, volume, includeLinkedZones, programUri, programMetaData)
56
+ options = {:StartLocalTime => startLocalTime, :Duration => duration,
57
+ :Recurrence => recurrence, :Enabled => enabled, :RoomUUID => roomUuid,
58
+ :PlayMode => playMode, :Volume => volume, :IncludeLinkedZones => includeLinkedZones,
59
+ :ProgramURI => programUri, :ProgramMetaData => programMetaData}
60
+ #options = {:StartLocalTime => '13:19:00', :Duration => '02:00:00',
61
+ # :Recurrence => 'ONCE', :Enabled => 1, :RoomUUID => 'RINCON_000E583564A601400',
62
+ # :PlayMode => 'SHUFFLE_NOREPEAT', :Volume => 25, :IncludeLinkedZones => 0,
63
+ # :ProgramURI => 'x-rincon-buzzer:0', :ProgramMetaData => ""}
64
+ send_alarm_message('CreateAlarm', convert_hash_to_xml(options))
65
+ end
66
+
67
+ def destroy_alarm(id)
68
+ send_alarm_message('DestroyAlarm', "<ID>#{id}</ID>")
69
+ end
70
+
71
+ def update_alarm(id, startLocalTime, duration, recurrence, enabled, roomUuid, playMode, volume, includeLinkedZones, programUri, programMetaData)
72
+ alarm_hash = {:ID => id, :StartLocalTime => startLocalTime, :Duration => duration,
73
+ :Recurrence => recurrence, :Enabled => enabled, :RoomUUID => roomUuid,
74
+ :PlayMode => playMode, :Volume => volume, :IncludeLinkedZones => includeLinkedZones,
75
+ :ProgramURI => programUri, :ProgramMetaData => programMetaData}
76
+ #options = {:ID => 8, :StartLocalTime => '13:19:00', :Duration => '02:00:00',
77
+ # :Recurrence => 'ONCE', :Enabled => 1, :RoomUUID => 'RINCON_000E583564A601400',
78
+ # :PlayMode => 'SHUFFLE_NOREPEAT', :Volume => 25, :IncludeLinkedZones => 0,
79
+ # :ProgramURI => 'x-rincon-buzzer:0', :ProgramMetaData => ""}
80
+ send_alarm_message('UpdateAlarm', convert_hash_to_xml(alarm_hash))
81
+ end
82
+
83
+ def is_alarm_enabled?(id)
84
+ list_alarms[id][:Enabled]
85
+ end
86
+
87
+ def enable_alarm(id)
88
+ alarm_hash = list_alarms[id]
89
+ alarm_hash[:Enabled] = '1'
90
+ send_alarm_message('UpdateAlarm', convert_hash_to_xml(alarm_hash))
91
+ end
92
+
93
+ def disable_alarm(id)
94
+ alarm_hash = list_alarms[id]
95
+ alarm_hash[:Enabled] = '0'
96
+ send_alarm_message('UpdateAlarm', convert_hash_to_xml(alarm_hash))
97
+ end
98
+
99
+ def set_alarm_volume(id, volume)
100
+ alarm_hash = list_alarms[id]
101
+ alarm_hash[:Volume] = volume
102
+ send_alarm_message('UpdateAlarm', convert_hash_to_xml(alarm_hash))
103
+ end
104
+
105
+ private
106
+
107
+ def alarm_client
108
+ @alarm_client ||= Savon.client endpoint: "http://#{self.ip}:#{Sonos::PORT}#{ALARM_CLOCK_ENDPOINT}", namespace: Sonos::NAMESPACE
109
+ end
110
+
111
+ def send_alarm_message(name, part = '')
112
+ action = "#{ALARM_CLOCK_XMLNS}##{name}"
113
+ message = %Q{<u:#{name} xmlns:u="#{ALARM_CLOCK_XMLNS}">#{part}</u:#{name}>}
114
+ alarm_client.call(name, soap_action: action, message: message)
115
+ end
116
+
117
+ def convert_hash_to_xml(options = {})
118
+ updatePart = ''
119
+ options.each do |optionKey, optionValue|
120
+ updatePart += "<#{optionKey}>#{optionValue}</#{optionKey}>"
121
+ end
122
+ updatePart
123
+ end
124
+
125
+ end
@@ -31,7 +31,7 @@ module Sonos::Endpoint::ContentDirectory
31
31
  private
32
32
 
33
33
  def content_directory_client
34
- @content_directory_client ||= Savon.client endpoint: "http://#{self.ip}:#{Sonos::PORT}#{CONTENT_DIRECTORY_ENDPOINT}", namespace: Sonos::NAMESPACE
34
+ @content_directory_client ||= Savon.client endpoint: "http://#{self.ip}:#{Sonos::PORT}#{CONTENT_DIRECTORY_ENDPOINT}", namespace: Sonos::NAMESPACE, log_level: :error
35
35
  end
36
36
 
37
37
  def parse_items(string)
@@ -11,7 +11,7 @@ module Sonos::Endpoint::Device
11
11
  private
12
12
 
13
13
  def device_client
14
- @device_client ||= Savon.client endpoint: "http://#{self.ip}:#{Sonos::PORT}#{DEVICE_ENDPOINT}", namespace: Sonos::NAMESPACE
14
+ @device_client ||= Savon.client endpoint: "http://#{self.ip}:#{Sonos::PORT}#{DEVICE_ENDPOINT}", namespace: Sonos::NAMESPACE, log_level: :error
15
15
  end
16
16
 
17
17
  def send_device_message(name, value)
@@ -82,7 +82,7 @@ private
82
82
  end
83
83
 
84
84
  def rendering_client
85
- @rendering_client ||= Savon.client endpoint: "http://#{self.ip}:#{Sonos::PORT}#{RENDERING_ENDPOINT}", namespace: Sonos::NAMESPACE
85
+ @rendering_client ||= Savon.client endpoint: "http://#{self.ip}:#{Sonos::PORT}#{RENDERING_ENDPOINT}", namespace: Sonos::NAMESPACE, log_level: :error
86
86
  end
87
87
 
88
88
  def send_rendering_message(name, value = nil)
@@ -0,0 +1,79 @@
1
+ require 'httpclient'
2
+
3
+ module Sonos::Endpoint::Upnp
4
+ UPNP_TIMEOUT = 600
5
+
6
+ ENDPOINT = {
7
+ content_directory: '/MediaServer/ContentDirectory/Event',
8
+ av_transport: '/MediaRenderer/AVTransport/Event',
9
+ zone_group_topology: '/ZoneGroupTopology/Event',
10
+ device_properties: '/DeviceProperties/Event',
11
+ group_management: '/GroupManagement/Event',
12
+ group_rendering_control: '/MediaRenderer/GroupRenderingControl/Event',
13
+ rendering_control: '/MediaRenderer/RenderingControl/Event',
14
+ connection_manager: '/MediaRenderer/ConnectionManager/Event',
15
+ queue: '/MediaRenderer/Queue/Event',
16
+ alarm_clock: '/AlarmClock/Event',
17
+ music_services: '/MusicServices/Event',
18
+ system_properties: '/SystemProperties/Event'
19
+ }.freeze
20
+
21
+ # Subscribes to UPNP events, with callbacks being sent to the provided URL
22
+ # @param [String] The URL to receive http callbacks from the device
23
+ # @param [Symbol] The [Sonos::Endpoint::Upnp::ENDPOINT] to subscribe to
24
+ # @return [Hash] Returns the timeout for the HTTP listener as well as the device SID
25
+ def subscribe_to_upnp_events(callback_url, event)
26
+ client = HTTPClient.new
27
+
28
+ request_headers = {
29
+ # The URL to be requested when a callback happens
30
+ 'CALLBACK' => "<#{callback_url}>",
31
+ # Apparently required in order to appease the UPNP gods
32
+ 'NT' => 'upnp:event',
33
+ # The timeout for the subscription - set to 10 minutes
34
+ 'TIMEOUT' => "Second-#{UPNP_TIMEOUT}"
35
+ }
36
+
37
+ response = client.request(:subscribe, event_url(event), header: request_headers)
38
+
39
+ # Convert the resulting headers into something less shouty
40
+ result_headers = response.header.all.inject({}) do |result, item|
41
+ result[item[0].downcase.to_sym] = item[1]
42
+ result
43
+ end
44
+
45
+ # Return all the information you'd need in order to do an unsusbscribe
46
+ Subscription.new({
47
+ timeout: result_headers[:timeout][/(\d+)/].to_i,
48
+ sid: result_headers[:sid],
49
+ event: event
50
+ })
51
+ end
52
+
53
+ # Unsubscribes an existing UPNP event
54
+ # @param [String] The subscription ID that you wish to cancel
55
+ # @param [Symbol] The [Sonos::Endpoint::Upnp::ENDPOINT] to which it belongs
56
+ def unsubscribe_from_upnp_events(subscription)
57
+ HTTPClient.new.request(:unsubscribe, event_url(subscription.event), header: {'SID' => subscription.sid })
58
+ end
59
+
60
+ # Simple class structure for describing an active subscription
61
+ class Subscription
62
+ attr_accessor :timeout
63
+ attr_accessor :sid
64
+ attr_accessor :event
65
+
66
+ def initialize(hash)
67
+ hash.each do |k, v|
68
+ self.send("#{k}=", v) if respond_to?(k)
69
+ end
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def event_url(event)
76
+ "http://#{self.ip}:#{Sonos::PORT}#{ENDPOINT[event]}"
77
+ end
78
+
79
+ end
@@ -73,26 +73,38 @@ module Sonos
73
73
  @devices = @topology.collect(&:device)
74
74
 
75
75
  construct_groups
76
+
77
+ speakers.each do |speaker|
78
+ speaker.group_master = speaker
79
+ @groups.each do |group|
80
+ speaker.group_master = group.master_speaker if group.master_speaker.uid == speaker.uid
81
+ group.slave_speakers.each do |slave|
82
+ speaker.group_master = group.master_speaker if slave.uid == speaker.uid
83
+ end
84
+ end
85
+ end
76
86
  end
77
-
87
+
78
88
  private
79
89
 
80
90
  def construct_groups
81
91
  # Loop through all of the unique groups
82
92
  @topology.collect(&:group).uniq.each do |group_uid|
83
- master_uuid = group_uid.split(':').first
84
- nodes = []
85
- master = nil
86
93
 
94
+ # set group's master nade using topology's coordinator parameter
95
+ master = nil
87
96
  @topology.each do |node|
88
- # Select all of the nodes with this group uid
97
+ # Select only the nodes with this group uid
89
98
  next unless node.group == group_uid
99
+ master = node if node.coordinator == "true"
100
+ end
90
101
 
91
- if node.uuid == master_uuid
92
- master = node
93
- else
94
- nodes << node
95
- end
102
+ # register other nodes in groups as slave nodes
103
+ nodes = []
104
+ @topology.each do |node|
105
+ # Select only the nodes with this group uid
106
+ next unless node.group == group_uid
107
+ nodes << node unless node.uuid == master.uuid
96
108
  end
97
109
 
98
110
  # Skip this group if there are no nodes or master
@@ -1,3 +1,3 @@
1
1
  module Sonos
2
- VERSION = '0.3.3'
2
+ VERSION = '0.3.4'
3
3
  end
@@ -19,7 +19,8 @@ Gem::Specification.new do |gem|
19
19
  gem.require_paths = ['lib']
20
20
 
21
21
  gem.required_ruby_version = '>= 1.9.2'
22
- gem.add_dependency 'savon', '~> 2.0.2'
22
+ gem.add_dependency 'savon', '~> 2.0'
23
23
  gem.add_dependency 'nokogiri'
24
24
  gem.add_dependency 'thor'
25
+ gem.add_dependency 'httpclient'
25
26
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sonos
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Soffes
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-06-29 00:00:00.000000000 Z
12
+ date: 2013-11-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: savon
@@ -17,14 +17,14 @@ dependencies:
17
17
  requirements:
18
18
  - - ~>
19
19
  - !ruby/object:Gem::Version
20
- version: 2.0.2
20
+ version: '2.0'
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - ~>
26
26
  - !ruby/object:Gem::Version
27
- version: 2.0.2
27
+ version: '2.0'
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: nokogiri
30
30
  requirement: !ruby/object:Gem::Requirement
@@ -53,6 +53,20 @@ dependencies:
53
53
  - - '>='
54
54
  - !ruby/object:Gem::Version
55
55
  version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: httpclient
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
56
70
  description: Control Sonos speakers with Ruby
57
71
  email:
58
72
  - sam@soff.es
@@ -63,7 +77,6 @@ extensions: []
63
77
  extra_rdoc_files: []
64
78
  files:
65
79
  - .gitignore
66
- - .ruby-version
67
80
  - .travis.yml
68
81
  - Changelog.markdown
69
82
  - Gemfile
@@ -74,15 +87,17 @@ files:
74
87
  - lib/sonos.rb
75
88
  - lib/sonos/cli.rb
76
89
  - lib/sonos/device.rb
90
+ - lib/sonos/device/accessory.rb
77
91
  - lib/sonos/device/base.rb
78
- - lib/sonos/device/bridge.rb
79
92
  - lib/sonos/device/speaker.rb
80
93
  - lib/sonos/discovery.rb
81
94
  - lib/sonos/endpoint.rb
82
95
  - lib/sonos/endpoint/a_v_transport.rb
96
+ - lib/sonos/endpoint/alarm.rb
83
97
  - lib/sonos/endpoint/content_directory.rb
84
98
  - lib/sonos/endpoint/device.rb
85
99
  - lib/sonos/endpoint/rendering.rb
100
+ - lib/sonos/endpoint/upnp.rb
86
101
  - lib/sonos/group.rb
87
102
  - lib/sonos/system.rb
88
103
  - lib/sonos/topology_node.rb
@@ -113,7 +128,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
113
128
  version: '0'
114
129
  requirements: []
115
130
  rubyforge_project:
116
- rubygems_version: 2.0.2
131
+ rubygems_version: 2.0.3
117
132
  signing_key:
118
133
  specification_version: 4
119
134
  summary: Control Sonos speakers with Ruby
@@ -1 +0,0 @@
1
- 2.0.0-p195
@@ -1,17 +0,0 @@
1
- require 'savon'
2
- require 'sonos/endpoint'
3
-
4
- module Sonos::Device
5
-
6
- # Used for Zone Bridge
7
- class Bridge < Base
8
-
9
- MODEL_NUMBERS = ['ZB100','ZP80']
10
-
11
- attr_reader :icon
12
-
13
- def self.model_numbers
14
- MODEL_NUMBERS
15
- end
16
- end
17
- end