Project

easy_upnp

0.01
No commit activity in last 3 years
No release in over 3 years
There's a lot of open issues
A super easy to use UPnP control point client
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies

Runtime

~> 1.8
~> 2.6
>= 0
~> 0.3
~> 2.11
 Project Readme

easy_upnp Gem Version Build Status

A super simple UPnP control point client for Ruby

Installing

easy_upnp is available on Rubygems. You can install it with:

$ gem install easy_upnp

You can also add it to your Gemfile:

gem 'easy_upnp'

Example usage

Find devices with SSDP

Simple Service Discovery Protocol (SSDP) is a simple UDP protocol used to discover services on a network. It's the entry point to create control points in easy_upnp.

The search method takes one argument -- the "search target". This controls a header sent in the SSDP packet which affects the devices that respond to the search query. You can use 'ssdp:all' to specify that all devices should respond.

require 'easy_upnp'

searcher = EasyUpnp::SsdpSearcher.new
devices = searcher.search 'ssdp:all'

This will return a list of EasyUpnp::UpnpDevice objects. You'll use these to interact with devices on your network.

Interacting with a specific device

Once you have a EasyUpnp::UpnpDevice, you can start interacting with the services it advertizes. To get a list of all services a device supports:

device = devices.first
device.all_services
# => ["urn:schemas-upnp-org:service:ContentDirectory:1", "urn:schemas-upnp-org:service:ConnectionManager:1", "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1"]

You can then create a service client and make calls to the service:

service = device.service 'urn:schemas-upnp-org:service:ContentDirectory:1'

service.service_methods
# => ["GetSearchCapabilities", "GetSortCapabilities", "GetSystemUpdateID", "Browse", "Search"]

service.GetSystemUpdateID
# => {:Id=>"207"}

Get information about a specific device

device.description
# => { ... } # all available information

device.description['friendlyName']
# => "WeMo Switch"

device.description['deviceType']
# => "urn:Belkin:device:controllee:1"

device.description['serialNumber']
# => "221520K1100836"

Static client construction

After you've constructed a client (DeviceControlPoint), you probably don't want to have to use SSDP to construct it again the next time you use it. DeviceControlPoint is equipped with #to_params and #from_params methods to make this easy.

Say you have a client called client. To dump it into a hash, do the following:

params = client.to_params
#=> {:urn=>"urn:schemas-upnp-org:service:ContentDirectory:1", :service_endpoint=>"http://10.133.8.11:8200/ctl/ContentDir", :definition=>"<?xml version=\"1.0\"?>\r\n<scpd xmlns=\"urn:schemas-upnp-org:service-1-0\">( ... clipped ... )</scpd>", :options=>{}}

We can then reconstruct a client from these params and use it normally:

client = EasyUpnp::DeviceControlPoint.from_params(params)
client.GetSystemUpdateID
=> {:Id=>"258"}

Logging

By default, logs will be printed to $stdout at the :error level. To change this behavior, you can use the following options when constructing a control point:

service = client.service(
  'urn:schemas-upnp-org:service:ContentDirectory:1',
  log_enabled: true,
  log_level: :info
)

service = client.service('urn:schemas-upnp-org:service:ContentDirectory:1') do |s|
  s.log_enabled = true
  s.log_level = :debug
end

Validation

Clients can validate the arguments passed to its methods. By default, this behavior is disabled. You can enable it when initializing a client:

client = device.service('urn:schemas-upnp-org:service:ContentDirectory:1') do |o|
  o.validate_arguments = true
end

This enables type checking in addition to whatever validation information is available in the UPnP service's definition. For example:

client.GetVolume(InstanceID: '0', Channel: 'Master')
#: ArgumentError: Invalid value for argument InstanceID: 0 is the wrong type. Should be one of: [Integer]
client.GetVolume(InstanceID: 0, Channel: 'Master2')
#: ArgumentError: Invalid value for argument Channel: Master2 is not in list of allowed values: ["Master"]
client.GetVolume(InstanceID: 0, Channel: 'Master')
#=> {:CurrentVolume=>"32"}

It's also possible to retrieve information about arguments:

client.method_args(:SetVolume)
#=> [:InstanceID, :Channel, :DesiredVolume]
validator = client.arg_validator(:SetVolume, :DesiredVolume)
validator.required_class
#=> Integer
validator.valid_range
#=> #<Enumerator: 0..100:step(1)>
validator.valid_range.max
#=> 100
validator.validate(32)
#=> true
validator.validate(101)
#: ArgumentError: 101 is not in allowed range of values: #<Enumerator: 0..100:step(1)>

validator = client.arg_validator(:SetVolume, :Channel)
validator.allowed_values
#=> ["Master"]

Events

easy_upnp allows you to subscribe to events. UPnP events are supported by registering HTTP callbacks with services. You can read more about the specifics in Section 4 of the UPnP Device Architecture document. Using this you could, for example, receive events when the volume or mute state changes on your UPnP-enabled TV. You might see something like this HTTP request, for example:

NOTIFY / HTTP/1.1
Host: 192.168.1.100:8888
Date: Sun, 17 Apr 2016 07:40:01 GMT
User-Agent: UPnP/1.0
Content-Type: text/xml; charset="utf-8"
Content-Length: 479
NT: upnp:event
NTS: upnp:propchange
SID: uuid:9742fed0-046f-11e6-8000-fcf1524b4f9c
SEQ: 0

<?xml version="1.0"?><e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0"><e:property><LastChange>&lt;Event xmlns=&quot;urn:schemas-upnp-org:metadata-1-0/RCS/&quot;&gt;
  &lt;InstanceID val=&quot;0&quot;&gt;
    &lt;PresetNameList val=&quot;FactoryDefaults&quot;/&gt;
    &lt;Mute val=&quot;0&quot; channel=&quot;Master&quot;/&gt;
    &lt;Volume val=&quot;34&quot; channel=&quot;Master&quot;/&gt;
  &lt;/InstanceID&gt;
&lt;/Event&gt;
</LastChange></e:property></e:propertyset>

There are two ways you can subscribe to events with easy_upnp:

  1. Registering a custom HTTP endpoint.
  2. Providing a callback lambda or Proc which is called each time an event is fired.

In the case of (2), easy_upnp behind the scenes starts a WEBrick HTTP server, which calls the provided callback whenever it receives an HTTP NOTIFY request.

Because event subscriptions expire, easy_upnp starts a background thread to renew the subscription on an interval.

Calling URLs

To add a URL to be called on events:

# Registers the provided URL with the service. If everything works appropriately, this
# URL will be called with HTTP NOTIFY requests from the service.
manager = service.add_event_callback('http://myserver/path/to/callback')

# The object that's returned allows you to manage the event subscription. To
# cancel the subscription, for example:
manager.unsubscribe

# You can also start the subscription after unsubscribing:
manager.subscribe

# Or get the subscription identifier:
manager.subscription_id
#=> "uuid:6ef254f0-04d1-11e6-8000-fcf1524b4f9c"

You can also construct a manager that attempts to manage an existing subscription:

manager = service.add_event_callback('http://myserver/path/to/callback') do |c|
  c.existing_sid = 'uuid:6ef254f0-04d1-11e6-8000-fcf1524b4f9c'
end

Calling ruby code

If you don't want to have to set up an HTTP endpoint to listen to events, you can have easy_upnp do it for you. The on_event starts an internal HTTP server on an ephemeral port behind the scenes and triggers the provided callback each time a request is recieved.

# Parse and print the XML body of the request
callback = ->(state_vars) do
  state_vars.map do |var, value|
    puts "#{var} ==>"
    puts Nokogiri::XML(value).to_xml
  end
end
manager = service.on_event(callback)

# End the subscription and shut down the internal HTTP server
manager.unsubscribe

# This will start a new HTTP server and start a new subscription
manager.subscribe

Here's an example of some output from the above callback:

LastChange ==>
<?xml version="1.0"?>
<Event xmlns="urn:schemas-upnp-org:metadata-1-0/RCS/">
  <InstanceID val="0">
    <PresetNameList val="FactoryDefaults"/>
    <Mute val="0" channel="Master"/>
    <Volume val="20" channel="Master"/>
  </InstanceID>
</Event>

While the default configurations are probably fine for most situations, you can configure both the internal HTTP server and the subscription manager when you call on_event by passing a configuration block:

manager = service.on_event(callback) do |c|
  c.configure_http_listener do |l|
    l.listen_port = 8888
    l.bind_address = '192.168.1.100'

    # Don't parse XML body, pass raw contents to callback
    l.event_parser = EasyUpnp::NoOpEventParser
  end

  c.configure_subscription_manager do |m|
    m.requested_timeout = 1800
    m.resubscription_interval_buffer = 60
    m.existing_sid = 'uuid:6ef254f0-04d1-11e6-8000-fcf1524b4f9c'
    m.log_level = Logger::INFO
  end
end