require 'uri'
require 'savon'
require_relative 'base'
module RUPNP
# Service class for device's services.
#
# ==Actions
# This class defines ruby methods from actions defined in
# service description, as provided by the device.
#
# By example, from this description:
#
# actionName
#
#
# argumentNameIn
# in
# stateVariableName
#
#
# argumentNameOut
# out
# stateVariableName
#
#
# a +#action_name+ method is created. This method requires a hash with
# an element named +argument_name_in+.
# If no in argument is required, an empty hash ({}
)
# must be passed to the method.
#
# A Hash is returned, with a key for each out argument.
#
# @author Sylvain Daubert
class CP::RemoteService < CP::Base
# @private
@@event_sub_count = 0
# Get event subscription count for all services
# (unique ID for subscription)
# @return [Integer]
def self.event_sub_count
@@event_sub_count += 1
end
# @private SOAP integer types
INTEGER_TYPES = %w(ui1 ui2 ui4 i1 i2 i4 int).freeze
# @private SOAP float types
FLOAT_TYPES = %w(r4 r8 number float).freeze
# @private SOAP string types
STRING_TYPES = %w(char string uuid).freeze
# @private SOAP true values
TRUE_TYPES = %w(1 true yes).freeze
# @private SOAP false values
FALSE_TYPES = %w(0 false no).freeze
# Get device to which this service belongs to
# @return [Device]
attr_reader :device
# Get service type
# @return [String]
attr_reader :type
# URL for service description
# @return [String]
attr_reader :scpd_url
# URL for control
# @return [String]
attr_reader :control_url
# URL for eventing
# @return [String]
attr_reader :event_sub_url
# XML namespace for device description
# @return [String]
attr_reader :xmlns
# Define architecture on which the service is implemented
# @return [String]
attr_reader :spec_version
# Available actions on this service
# @return [Array]
attr_reader :actions
# State table for the service
# @return [Array]
attr_reader :state_table
# @param [Device] device
# @param [String] url_base
# @param [Hash] service
def initialize(device, url_base, service)
super()
@device = device
@description = service
@type = service[:service_type].to_s
@scpd_url = build_url(url_base, service[:scpdurl].to_s)
@control_url = build_url(url_base, service[:control_url].to_s)
@event_sub_url = build_url(url_base, service[:event_sub_url].to_s)
@actions = []
initialize_savon
end
# Get service from its description
# @return [void]
def fetch
if @scpd_url.empty?
fail 'no SCPD URL'
return
end
scpd_getter = EM::DefaultDeferrable.new
scpd_getter.errback do
fail "cannot get SCPD from #@scpd_url"
end
scpd_getter.callback do |scpd|
if !scpd or scpd.empty?
fail "SCPD from #@scpd_url is empty"
next
end
if bad_description?(scpd)
fail 'not a UPNP 1.0/1.1 SCPD'
next
end
extract_service_state_table scpd
extract_actions scpd
succeed self
end
get_description @scpd_url, scpd_getter
end
# Subscribe to event
# @param [Hash] options
# @option options [Integer] timeout
# @yieldparam [Event] event event received
def subscribe_to_event(options={}, &blk)
cp = device.control_point
cp.start_event_server
port = cp.event_port
num = self.class.event_sub_count
@callback_url = "http://#{HOST_IP}:#{port}/event#{num}}"
uri = URI(@event_sub_url)
options[:timeout] ||= EVENT_SUB_DEFAULT_TIMEOUT
subscribe_req = < @type
locals.soap_action "#{type}##{action_name}"
if params
unless params.is_a? Hash
raise ArgumentError, 'only hash arguments are accepted'
end
locals.message params
end
end
if action[:arguments].is_a? Hash
log :debug, 'only one argument in argument list'
if action[:arguments][:direction] == 'out'
process_soap_response name, response, action[:arguments]
end
else
log :debug, 'true argument list'
action[:arguments].map do |arg|
if params arg[:direction] == 'out'
process_soap_response name, response, arg
end
end
end
end
end
def process_soap_response(action, resp, out_arg)
if resp.success? and resp.to_xml.empty?
log :debug, 'Successful SOAP request but empty response'
return {}
end
state_var = @state_table.find do |h|
h[:name] == out_arg[:related_state_variable]
end
action_response = "#{action}_response".to_sym
out_arg_name = snake_case(out_arg[:name]).to_sym
value = resp.hash[:envelope][:body][action_response][out_arg_name]
transform_method = if INTEGER_TYPES.include? state_var[:data_type]
:to_i
elsif FLOAT_TYPES.include? state_var[:data_type]
:to_f
elsif STRING_TYPES.include? state_var[:data_type]
:to_s
end
if transform_method
{ out_arg_name => value.send(transform_method) }
elsif TRUE_TYPES.include? state_var[:data_type]
{ out_arg_name => true }
elsif FALSE_TYPES.include? state_var[:data_type]
{ out_arg_name => false }
else
log :warn, "SOAP response has an unknown type: #{state_var[:data_type]}"
{}
end
end
def initialize_savon
@soap = Savon.client do |globals|
globals.log_level :error
globals.endpoint @control_url
globals.namespace @type
globals.convert_request_keys_to :camel_case
globals.log true
globals.headers :HOST => "#{HOST_IP}"
globals.env_namespace 's'
globals.namespace_identifier 'u'
end
end
end
end