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 +4 -4
- data/.gitignore +2 -0
- data/LICENSE +2 -2
- data/Rakefile +10 -0
- data/Readme.markdown +2 -2
- data/lib/sonos/cli.rb +30 -0
- data/lib/sonos/device.rb +1 -1
- data/lib/sonos/device/accessory.rb +20 -0
- data/lib/sonos/device/base.rb +14 -4
- data/lib/sonos/device/speaker.rb +77 -6
- data/lib/sonos/discovery.rb +1 -0
- data/lib/sonos/endpoint.rb +2 -0
- data/lib/sonos/endpoint/a_v_transport.rb +1 -3
- data/lib/sonos/endpoint/alarm.rb +125 -0
- data/lib/sonos/endpoint/content_directory.rb +1 -1
- data/lib/sonos/endpoint/device.rb +1 -1
- data/lib/sonos/endpoint/rendering.rb +1 -1
- data/lib/sonos/endpoint/upnp.rb +79 -0
- data/lib/sonos/system.rb +22 -10
- data/lib/sonos/version.rb +1 -1
- data/sonos.gemspec +2 -1
- metadata +22 -7
- data/.ruby-version +0 -1
- data/lib/sonos/device/bridge.rb +0 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f3ea780abc6b1575275463a67745acf4283db680
|
4
|
+
data.tar.gz: 7be54b8d1474a567b8e069f95b46ddd7b168b928
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4c31701f021123f74e93189c7348e5a79ce774384fd2c87613f919fcd8b9a3387c5fe1eaaeeeb3c0e3e3f6c33f5fdb09462fb3e9046cbb20ca3b6fe0adf2460f
|
7
|
+
data.tar.gz: 92b8c154f0c759f54e4d24fb51580668bad8b727244c8b51f1594b3f733081fec13d6d3de43592bd0058f3c3ad7826c181b7783f5d95355a62ab2e4d1902aa6e
|
data/.gitignore
CHANGED
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
|
data/Readme.markdown
CHANGED
@@ -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/
|
7
|
+
[![Code Climate](https://codeclimate.com/github/soffes/sonos.png)](https://codeclimate.com/github/soffes/sonos)
|
8
8
|
|
9
9
|
## Installation
|
10
10
|
|
data/lib/sonos/cli.rb
CHANGED
@@ -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
|
data/lib/sonos/device.rb
CHANGED
@@ -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
|
data/lib/sonos/device/base.rb
CHANGED
@@ -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,
|
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
|
-
|
13
|
-
|
14
|
-
|
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?
|
data/lib/sonos/device/speaker.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
data/lib/sonos/discovery.rb
CHANGED
@@ -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|
|
data/lib/sonos/endpoint.rb
CHANGED
@@ -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><Alarms><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"/></Alarms></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
|
data/lib/sonos/system.rb
CHANGED
@@ -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
|
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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
data/lib/sonos/version.rb
CHANGED
data/sonos.gemspec
CHANGED
@@ -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
|
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.
|
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-
|
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
|
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
|
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.
|
131
|
+
rubygems_version: 2.0.3
|
117
132
|
signing_key:
|
118
133
|
specification_version: 4
|
119
134
|
summary: Control Sonos speakers with Ruby
|
data/.ruby-version
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
2.0.0-p195
|
data/lib/sonos/device/bridge.rb
DELETED