sonos 0.3.3 → 0.3.4
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.
- 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
|
-
[](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