Project

easy_upnp

0.02
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
 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