class UPnP::Control::Service

A service on a UPnP control point.

A Service exposes the UPnP actions as ordinary ruby methods, which are handled via method_missing. A Service responds appropriately to respond_to? and methods to make introspection easy.

Services should be created using ::create instead of ::new. This allows a subclass of Service to be automatically instantiated.

When creating a service subclass, it must have a URN_version constant set to the schema URN for that version.

For details on UPnP services, see www.upnp.org/resources/documents.asp

Attributes

actions[R]

Hash mapping UPnP Actions to arguments

{
  'GetTotalPacketsSent' =>
    [['out', 'NewTotalPacketsSent', 'TotalPacketsSent']]
}
control_url[R]

Control URL

driver[R]

SOAP driver for this service

event_sub_url[R]

Eventing URL

id[R]

Service identifier, unique within this service's devices

scpd_url[R]

Service description URL

type[R]

UPnP service type

url[R]

Base URL for this service’s device

Public Class Methods

create(description, url) click to toggle source

If a concrete class exists for description it is used to instantiate the service, otherwise a concrete class is created subclassing Service and used.

# File lib/UPnP/control/service.rb, line 200
def self.create(description, url)
  type = description.at('serviceType').text.strip

  # HACK need vendor namespaces
  klass_name = type.sub(%rurn:[^:]+:service:([^:]+):.*/, '\1')

  begin
    klass = const_get klass_name
  rescue NameError
    klass = const_set klass_name, Class.new(self)
    klass.const_set :URN_1, "#{UPnP::SERVICE_SCHEMA_PREFIX}:#{klass.name}:1"
  end

  klass.new description, url
end
new(description, url) click to toggle source

Creates a new service from Nokogiri::XML::Element description and url. The description must be a service fragment from a device description.

# File lib/UPnP/control/service.rb, line 220
def initialize(description, url)
  @url = url

  @type = description.at('serviceType').text.strip
  @id = description.at('serviceId').text.strip
  @control_url = @url + description.at('controlURL').text.strip
  @event_sub_url = @url + description.at('eventSubURL').text.strip
  @scpd_url = @url + description.at('SCPDURL').text.strip

  create_driver
end

Public Instance Methods

create_driver() click to toggle source

Creates the SOAP driver from description at #scpd_url

# File lib/UPnP/control/service.rb, line 235
def create_driver
  parse_service_description

  @driver = SOAP::RPC::Driver.new @control_url, @type

  mapping_registry = UPnP::SOAPRegistry.new

  @actions.each do |name, arguments|
    soapaction = "#{@type}##{name}"
    qname = XSD::QName.new @type, name

    # TODO map ranges, enumerations
    arguments = arguments.map do |direction, arg_name, variable|
      type, = @variables[variable]

      schema_name = XSD::QName.new nil, arg_name

      mapping_registry.register :class => type, :schema_name => schema_name

      [direction, arg_name, @variables[variable].first]
    end

    @driver.proxy.add_rpc_method qname, soapaction, name, arguments
    @driver.send :add_rpc_method_interface, name, arguments
  end

  @driver.mapping_registry = mapping_registry

  @variables = nil
end
method_missing(message, *arguments) click to toggle source

Handles this service’s actions

# File lib/UPnP/control/service.rb, line 269
def method_missing(message, *arguments)
  return super unless respond_to? message

  begin
    @driver.send(message, *arguments)
  rescue SOAP::FaultError => e
    backtrace = caller 0

    fault_code = e.faultcode.data
    fault_string = e.faultstring.data

    detail = e.detail[fault_string]
    code = detail['errorCode'].to_i
    description = detail['errorDescription']

    backtrace.first.gsub!(%r:(\d+):in `([^']+)'/) do
      line = $1.to_i - 2
      ":#{line}:in `#{message}' (method_missing)"
    end

    e = UPnPError.new description, code
    e.set_backtrace backtrace
    raise e
  end
end
methods(include_ancestors = true) click to toggle source

Includes this service’s actions

# File lib/UPnP/control/service.rb, line 298
def methods(include_ancestors = true)
  super + @driver.methods(false)
end
parse_action_arguments(argument_list) click to toggle source

Extracts arguments for an action from argument_list

# File lib/UPnP/control/service.rb, line 305
def parse_action_arguments(argument_list)
  arguments = []

  argument_list.css('argument').each do |argument|
    name = argument.at('name').text.strip

    direction = argument.at('direction').text.strip.upcase
    direction = 'RETVAL' if argument.at 'retval'
    direction = SOAP::RPC::SOAPMethod.const_get direction
    variable  = argument.at('relatedStateVariable').text.strip

    arguments << [direction, name, variable]
  end if argument_list

  arguments
end
parse_actions(action_list) click to toggle source

Extracts service actions from action_list

# File lib/UPnP/control/service.rb, line 325
def parse_actions(action_list)
  @actions = {}

  action_list.css('action').each do |action|
    name = action.at('name').text.strip

    raise Error, "insecure action name #{name}" unless name =~ %r\A\w*\z/


    @actions[name] = parse_action_arguments action.at('argumentList')
  end
end
parse_allowed_value_list(state_variable) click to toggle source

Extracts a list of allowed values from state_variable

# File lib/UPnP/control/service.rb, line 341
def parse_allowed_value_list(state_variable)
  list = state_variable.at 'allowedValueList'

  return nil unless list

  values = []

  list.css('allowedValue').each do |value|
    value = value.text.strip
    raise Error, "insecure allowed value #{value}" unless value =~ %r\A\w*\z/
    values << value
  end

  values
end
parse_allowed_value_range(state_variable) click to toggle source

Extracts an allowed value range from state_variable

# File lib/UPnP/control/service.rb, line 360
def parse_allowed_value_range(state_variable)
  range = state_variable.at 'allowedValueRange'

  return nil unless range

  minimum = range.at 'minimum'
  maximum = range.at 'maximum'
  step    = range.at 'step'

  range = [minimum, maximum]
  range << step if step

  range.map do |value|
    value = value.text
    value =~ %r\./ ? Float(value) : Integer(value)
  end
end
parse_service_description() click to toggle source

Parses a service description from the #scpd_url

# File lib/UPnP/control/service.rb, line 381
def parse_service_description
  description = Nokogiri::XML open(@scpd_url)

  validate_scpd description

  parse_actions description.at('scpd > actionList')

  service_state_table = description.at 'scpd > serviceStateTable'
  parse_service_state_table service_state_table
rescue OpenURI::HTTPError
  raise Error, "Unable to open SCPD at #{@scpd_url.inspect} from device #{@url.inspect}"
end
parse_service_state_table(service_state_table) click to toggle source

Extracts state variables from service_state_table

# File lib/UPnP/control/service.rb, line 397
def parse_service_state_table(service_state_table)
  @variables = {}

  service_state_table.css('stateVariable').each do |var|
    name = var.at('name').text.strip
    data_type = Types::MAP[var.at('dataType').text.strip]
    default = var.at 'defaultValue'

    if default then
      default = default.text.strip
      raise Error, "insecure default value #{default}" unless
        default =~ %r\A\w*\z/
    end

    allowed_value_list  = parse_allowed_value_list var
    allowed_value_range = parse_allowed_value_range var

    @variables[name] = [
      data_type,
      default,
      allowed_value_list,
      allowed_value_range
    ]
  end
end
respond_to?(message) click to toggle source

Returns true for this service’s actions as well as the usual behavior

# File lib/UPnP/control/service.rb, line 426
def respond_to?(message)
  @driver.methods(false).include? message.to_s || super
end
validate_scpd(service_description) click to toggle source

Ensures service_description has the correct namespace, root element, and version numbers. Raises an exception if the service isn’t valid.

# File lib/UPnP/control/service.rb, line 434
def validate_scpd(service_description)
  namespace = service_description.at('scpd').namespace.href

  raise Error, "invalid namespace #{namespace}" unless
    namespace == 'urn:schemas-upnp-org:service-1-0'

  major = service_description.at('scpd > specVersion > major').text.strip
  minor = service_description.at('scpd > specVersion > minor').text.strip

  raise Error, "invalid version #{major}.#{minor}" unless
    major == '1' and minor == '0'
end