Permalink
Browse files

Initial commit

  • Loading branch information...
soffes committed Dec 23, 2012
0 parents commit 59d4f5d51f2d16ee8e506542e39586bc7e5e9f0c
Showing with 267 additions and 0 deletions.
  1. +17 −0 .gitignore
  2. +6 −0 Gemfile
  3. +22 −0 LICENSE
  4. +1 −0 Rakefile
  5. +62 −0 Readme.markdown
  6. +11 −0 lib/sonos.rb
  7. +124 −0 lib/sonos/speaker.rb
  8. +3 −0 lib/sonos/version.rb
  9. +21 −0 sonos.gemspec
@@ -0,0 +1,17 @@
+*.gem
+*.rbc
+.bundle
+.config
+.yardoc
+Gemfile.lock
+InstalledFiles
+_yardoc
+coverage
+doc/
+lib/bundler/man
+pkg
+rdoc
+spec/reports
+test/tmp
+test/version_tmp
+tmp
@@ -0,0 +1,6 @@
+source 'https://rubygems.org'
+
+# Specify your gem's dependencies in control.gemspec
+gemspec
+
+gem 'rake'
22 LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2012 Sam Soffes
+
+MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1 @@
+require 'bundler/gem_tasks'
@@ -0,0 +1,62 @@
+# Sonos
+
+Control Sonos speakers with Ruby.
+
+Huge thanks to [Rahim Sonawalla](https://github.com/rahims) for making [SoCo](https://github.com/rahims/SoCo). Control would not be possible without his work.
+
+## Installation
+
+Add this line to your application's Gemfile:
+
+ gem 'sonos'
+
+And then execute:
+
+ $ bundle
+
+Or install it yourself as:
+
+ $ gem install sonos
+
+## Usage
+
+I'm working on a CLI client. For now, we'll use IRB. You will need the IP address of a speaker (auto-detection is on my list too). To get the IP of a speaker, one of your Sonos controllers and go to "About My Sonos System".
+
+``` shell
+$ gem install sonos
+$ irb
+```
+
+``` ruby
+> require 'rubygems'
+> require 'sonos'
+> speaker = Sonos::Speaker('10.0.1.10') # or whatever the IP is
+```
+
+Now that we have a reference to the speaker, we can do all kinds of stuff.
+
+``` ruby
+> speaker.pause # Pause whatever is playing
+> speaker.play # Resumes the playlist
+> speaker.play 'http://assets.samsoff.es/music/Airports.mp3' # Stream!
+> speaker.now_playing
+> speaker.volume
+> speaker.volume = 70
+> speaker.volume -= 10
+```
+
+## Todo
+
+* Fix album art in `now_playing`
+* Handle line-in in `now_playing`
+* Auto-discovery
+* Better support for stero pairs
+* CLI client
+
+## Contributing
+
+1. Fork it
+2. Create your feature branch (`git checkout -b my-new-feature`)
+3. Commit your changes (`git commit -am 'Add some feature'`)
+4. Push to the branch (`git push origin my-new-feature`)
+5. Create new Pull Request
@@ -0,0 +1,11 @@
+require 'sonos/version'
+require 'sonos/speaker'
+
+module Sonos
+ PORT = 1400.freeze
+ NAMESPACE = 'http://www.sonos.com/Services/1.1'.freeze
+
+ def self.Speaker(ip)
+ Speaker.new(ip)
+ end
+end
@@ -0,0 +1,124 @@
+require 'savon'
+
+module Sonos
+ class Speaker
+ TRANSPORT_ENDPOINT = '/MediaRenderer/AVTransport/Control'.freeze
+ RENDERING_ENDPOINT = '/MediaRenderer/RenderingControl/Control'.freeze
+ DEVICE_ENDPOINT = '/DeviceProperties/Control'.freeze
+
+ attr_accessor :zone_name, :zone_icon, :uid, :serial_number, :software_version, :hardware_version, :mac_address
+
+ def initialize(ip)
+ @ip = ip
+
+ # Get meta data
+ self.get_speaker_info
+ end
+
+ #
+ # Get information about the currently playing track.
+ #
+ def get_position_info
+ action = 'urn:schemas-upnp-org:service:AVTransport:1#GetPositionInfo'
+ message = '<u:GetPositionInfo xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"><InstanceID>0</InstanceID><Channel>Master</Channel></u:GetPositionInfo>'
+ response = transport_client.call(:get_position_info, soap_action: action, message: message)
+ body = response.body[:get_position_info_response]
+ doc = Nokogiri::XML(body[:track_meta_data])
+
+ {
+ 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],
+ track_duration: body[:track_duration],
+ current_position: body[:rel_time],
+ uri: body[:track_uri],
+ album_art: "http://#{@ip}:#{PORT}#{doc.xpath('//upnp:albumArtURI').inner_text}"
+ }
+ end
+ alias_method :now_playing, :get_position_info
+
+ #
+ # Pause the currently playing track.
+ #
+ def pause
+ action = 'urn:schemas-upnp-org:service:AVTransport:1#Pause'
+ message = '<u:Pause xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"><InstanceID>0</InstanceID><Speed>1</Speed></u:Pause>'
+ transport_client.call(:play, soap_action: action, message: message)
+ end
+
+ #
+ # Play the currently selected track or play a stream.
+ #
+ def play(uri = nil)
+ # Play a song from the uri
+ if uri
+ self.set_av_transport_uri(uri)
+ return
+ end
+
+ # Play the currently selected track
+ action = 'urn:schemas-upnp-org:service:AVTransport:1#Play'
+ message = '<u:Play xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"><InstanceID>0</InstanceID><Speed>1</Speed></u:Play>'
+ transport_client.call(:play, soap_action: action, message: message)
+ end
+
+ #
+ # Play a stream.
+ #
+ def set_av_transport_uri(uri)
+ action = 'urn:schemas-upnp-org:service:AVTransport:1#SetAVTransportURI'
+ message = %Q{<u:SetAVTransportURI xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"><InstanceID>0</InstanceID><CurrentURI>#{uri}</CurrentURI><CurrentURIMetaData></CurrentURIMetaData></u:SetAVTransportURI>}
+ transport_client.call(:set_av_transport_uri, soap_action: action, message: message)
+ self.play
+ end
+ alias_method :play_stream, :set_av_transport_uri
+
+ #
+ # Get information about the Sonos speaker.
+ #
+ def get_speaker_info
+ doc = Nokogiri::XML(open("http://#{@ip}:1400/status/zp"))
+
+ self.zone_name = doc.xpath('.//ZoneName').inner_text
+ self.zone_icon = doc.xpath('.//ZoneIcon').inner_text
+ self.uid = doc.xpath('.//LocalUID').inner_text
+ self.serial_number = doc.xpath('.//SerialNumber').inner_text
+ self.software_version = doc.xpath('.//SoftwareVersion').inner_text
+ self.hardware_version = doc.xpath('.//HardwareVersion').inner_text
+ self.mac_address = doc.xpath('.//MACAddress').inner_text
+ self
+ end
+
+ #
+ # Get the current volume.
+ #
+ def get_volume
+ action = 'urn:schemas-upnp-org:service:RenderingControl:1#GetVolume'
+ message = '<u:GetVolume xmlns:u="urn:schemas-upnp-org:service:RenderingControl:1"><InstanceID>0</InstanceID><Channel>Master</Channel></u:GetVolume>'
+ response = rendering_client.call(:get_volume, soap_action: action, message: message)
+ response.body[:get_volume_response][:current_volume].to_i
+ end
+ alias_method :volume, :get_volume
+
+ #
+ # Set the volume from 0 to 100.
+ #
+ def set_volume(level)
+ action = 'urn:schemas-upnp-org:service:RenderingControl:1#SetVolume'
+ message = %Q{<u:SetVolume xmlns:u="urn:schemas-upnp-org:service:RenderingControl:1"><InstanceID>0</InstanceID><Channel>Master</Channel><DesiredVolume>#{level}</DesiredVolume></u:SetVolume>}
+ rendering_client.call(:set_volume, soap_action: action, message: message)
+ end
+ alias_method :volume=, :set_volume
+
+ private
+
+ def transport_client
+ @transport_client ||= Savon.client endpoint: "http://#{@ip}:#{PORT}#{TRANSPORT_ENDPOINT}", namespace: NAMESPACE
+ end
+
+ def rendering_client
+ @rendering_client ||= Savon.client endpoint: "http://#{@ip}:#{PORT}#{RENDERING_ENDPOINT}", namespace: NAMESPACE
+ end
+ end
+end
@@ -0,0 +1,3 @@
+module Sonos
+ VERSION = '0.0.1'
+end
@@ -0,0 +1,21 @@
+# -*- encoding: utf-8 -*-
+lib = File.expand_path('../lib', __FILE__)
+$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
+require 'sonos/version'
+
+Gem::Specification.new do |gem|
+ gem.name = 'sonos'
+ gem.version = Sonos::VERSION
+ gem.authors = ['Sam Soffes']
+ gem.email = ['sam@soff.es']
+ gem.description = 'Sonos Controller'
+ gem.summary = 'Control Sonos speakers with Ruby'
+ gem.homepage = 'https://github.com/soffes/sonos'
+
+ gem.files = `git ls-files`.split($/)
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
+ gem.require_paths = ['lib']
+
+ gem.add_dependency 'savon', '~> 2.0.2'
+end

0 comments on commit 59d4f5d

Please sign in to comment.