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