rupnp 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,7 +6,7 @@ require 'eventmachine-le'
6
6
  module RUPNP
7
7
 
8
8
  # RUPNP version
9
- VERSION = '0.2.2'
9
+ VERSION = '0.3.0'
10
10
 
11
11
  @logdev = STDERR
12
12
  @log_level = :info
@@ -57,7 +57,6 @@ require_relative 'rupnp/cp/base'
57
57
  require_relative 'rupnp/cp/remote_service'
58
58
  require_relative 'rupnp/cp/remote_device'
59
59
  require_relative 'rupnp/cp/event_server'
60
- require_relative 'rupnp/cp/event_subscriber'
61
60
  require_relative 'rupnp/ssdp'
62
61
 
63
62
  require_relative 'rupnp/device'
@@ -24,10 +24,6 @@ module RUPNP
24
24
  # Get event listening port
25
25
  # @return [Integer]
26
26
  attr_reader :event_port
27
- # Return channel to add event URL (URL to listen for a specific
28
- # event)
29
- # @return [EM::Channel]
30
- attr_reader :add_event_url
31
27
  # Return remote devices controlled by this control point
32
28
  # @return [Array<CP::RemoteDevice>]
33
29
  attr_reader :devices
@@ -48,7 +44,6 @@ module RUPNP
48
44
  @devices = []
49
45
  @new_device_channel = EM::Channel.new
50
46
  @bye_device_channel = EM::Channel.new
51
- @add_event_url = EM::Channel.new
52
47
  end
53
48
 
54
49
  # Start control point.
@@ -79,8 +74,7 @@ module RUPNP
79
74
  # @return [void]
80
75
  def start_event_server(port=EVENT_SUB_DEFAULT_PORT)
81
76
  @event_port ||= port
82
- @event_server ||= EM.start_server('0.0.0.0', port, CP::EventServer,
83
- @add_event_url)
77
+ @event_server ||= EM.start_server('0.0.0.0', port, CP::EventServer)
84
78
  end
85
79
 
86
80
  # Stop event server
@@ -7,44 +7,104 @@ module RUPNP
7
7
  class CP::EventServer < EM::HttpServer::Server
8
8
  include LogMixin
9
9
 
10
- # Channel to add url for listening to
11
- # @return [EM::Channel]
12
- attr_reader :add_url
13
-
10
+ # @private
11
+ @@add_event = EM::Channel.new
12
+ # @private
13
+ @@events = []
14
14
 
15
- # @param [EM::Channel] add_url_channel channel for adding url
16
- def initialize(add_url_channel)
17
- super
15
+ @@add_event.subscribe do |url|
16
+ @@events << url
17
+ end
18
18
 
19
- @urls = []
20
- @add_url = add_url_channel
19
+ class << self
20
+ # Channel to add url for listening to
21
+ # @param [Event] event
22
+ # @return [void]
23
+ def add_event(event)
24
+ @@add_event << event
25
+ end
21
26
 
22
- @add_url.subscribe do |url|
23
- log :info, "add URL #{url} for eventing"
24
- @urls << url
27
+ # Remove an event
28
+ # @param [Event] event
29
+ # @return [void]
30
+ def remove_event(event)
31
+ @@events.delete_if { |e| e == event }
25
32
  end
26
33
  end
27
34
 
35
+
36
+ # Channel to return received updated variables from events
37
+ # @return [EM::Channel]
38
+ attr_reader :events
39
+
40
+
28
41
  # Process a HTTP request received from a service/device
29
42
  def process_http_request
30
- log :debug, 'EventServer: receive request'
31
- url, event = @urls.find { |a| a[0] == @http_request_uri }
32
-
33
- if event.is_a? EM::Channel
34
- if @http_request_method == 'NOTIFY'
35
- if @http[:nt] == 'upnp:event' and @http[:nts] == 'upnp:propchange'
36
- event << {
37
- :sid => @http[:sid],
38
- :seq => @http[:seq],
39
- :content => @http_content }
40
- else
41
- log :warn, 'EventServer: ' +
42
- "malformed NOTIFY event message:\n#@http_headers\n#@http_content"
43
- end
44
- else
45
- log :warn, "EventServer: unknown HTTP verb: #@http_request_method"
46
- end
43
+ log :debug, "EventServer: receive request: #@http"
44
+
45
+ response = EM::DelegatedHttpResponse.new(self)
46
+
47
+ if @http_request_method != 'NOTIFY'
48
+ log :info, "EventServer: method #@http_request_method not allowed"
49
+ response.status = 405
50
+ response.headers['Allow'] = 'NOTIFY'
51
+ response.send_response
52
+ return
53
+ end
54
+
55
+ event = @@events.find { |e| e.callback_url == @http_request_uri }
56
+
57
+ if event.nil?
58
+ log :info, "EventServer: Requested URI #@http_request_uri unknown"
59
+ response.status = 404
60
+ response.send_response
61
+ return
62
+ end
63
+
64
+ if !event.is_a? EM::Channel
65
+ log :error, "EventServer: internal error!"
66
+ response.status = 500
67
+ response.send_response
68
+ return
69
+ end
70
+
71
+ if !@http.has_key?(:nt) or !@http.has_key?(:nts)
72
+ log :warn, 'EventServer: malformed NOTIFY event message'
73
+ log :debug, "#@http_headers\n#@http_content"
74
+ response.status = 400
75
+ response.send_response
76
+ return
77
+ end
78
+
79
+ if @http[:nt] != 'upnp:event' or @http[:nts] != 'upnp:propchange' or
80
+ !@http.has_key?(:sid) or @http[:sid] == ''
81
+ log :warn, 'EventServer: precondition failed'
82
+ log :debug, "#@http_headers\n#@http_content"
83
+ response.status = 412
84
+ response.send_response
85
+ return
86
+ end
87
+
88
+ if event.sid != @http[:sid]
89
+ log :warn, 'EventServer: precondition failed (unknown SID)'
90
+ log :debug, "#@http_headers\n#@http_content"
91
+ response.status = 412
92
+ response.send_response
93
+ return
47
94
  end
95
+
96
+ seq = @http[:seq].nil? ? 0 : @http[:seq].to_i
97
+ notification = {
98
+ :seq => seq,
99
+ :content => Nori.new.parse(@http_content)
100
+ }
101
+
102
+ log :debug, "send notification #{notification.inspect}\n" +
103
+ " to #{event}"
104
+ event << notification
105
+
106
+ response_status = 200
107
+ response.send_response
48
108
  end
49
109
 
50
110
  end
@@ -3,304 +3,375 @@ require 'savon'
3
3
  require_relative 'base'
4
4
 
5
5
  module RUPNP
6
+ module CP
7
+
8
+ # Service class for device's services.
9
+ #
10
+ # ==Actions
11
+ # This class defines ruby methods from actions defined in
12
+ # service description, as provided by the device.
13
+ #
14
+ # By example, from this description:
15
+ # <action>
16
+ # <name>actionName</name>
17
+ # <argumentList>
18
+ # <argument>
19
+ # <name>argumentNameIn</name>
20
+ # <direction>in</direction>
21
+ # <relatedStateVariable>stateVariableName</relatedStateVariable>
22
+ # </argument>
23
+ # <argument>
24
+ # <name>argumentNameOut</name>
25
+ # <direction>out</direction>
26
+ # <relatedStateVariable>stateVariableName</relatedStateVariable>
27
+ # </argument>
28
+ # </action>
29
+ # a +#action_name+ method is created. This method requires a hash with
30
+ # an element named +argument_name_in+.
31
+ # If no <i>in</i> argument is required, an empty hash (<code>{}</code>)
32
+ # must be passed to the method. Hash keys may not be symbols.
33
+ #
34
+ # A Hash is returned, with a key for each <i>out</i> argument.
35
+ #
36
+ # ==Evented variables
37
+ # Some variables in state (see {#state_table}, +:@send_events variable
38
+ # attribute) are evented. Events to update these variables are received
39
+ # only after subscription. To subscribe, use {#subscribe_to_event}.
40
+ #
41
+ # After subscribing to events, state variables are automagically updated
42
+ # on events. Their value may be accessed through {#variables}.
43
+ #
44
+ # A block may be passed to {#subscribe_to_event} to do a user action
45
+ # on receiving events.
46
+ #
47
+ # service.subscribe_to_event do |msg|
48
+ # puts "receive #{msg}"
49
+ # end
50
+ #
51
+ # @author Sylvain Daubert
52
+ class RemoteService < Base
53
+
54
+ # @private
55
+ @@event_sub_count = 0
56
+
57
+ # Get event subscription count for all services
58
+ # (unique ID for subscription)
59
+ # @return [Integer]
60
+ def self.event_sub_count
61
+ @@event_sub_count += 1
62
+ end
6
63
 
7
- # Service class for device's services.
8
- #
9
- # ==Actions
10
- # This class defines ruby methods from actions defined in
11
- # service description, as provided by the device.
12
- #
13
- # By example, from this description:
14
- # <action>
15
- # <name>actionName</name>
16
- # <argumentList>
17
- # <argument>
18
- # <name>argumentNameIn</name>
19
- # <direction>in</direction>
20
- # <relatedStateVariable>stateVariableName</relatedStateVariable>
21
- # </argument>
22
- # <argument>
23
- # <name>argumentNameOut</name>
24
- # <direction>out</direction>
25
- # <relatedStateVariable>stateVariableName</relatedStateVariable>
26
- # </argument>
27
- # </action>
28
- # a +#action_name+ method is created. This method requires a hash with
29
- # an element named +argument_name_in+.
30
- # If no <i>in</i> argument is required, an empty hash (<code>{}</code>)
31
- # must be passed to the method. Hash keys may not be symbols.
32
- #
33
- # A Hash is returned, with a key for each <i>out</i> argument.
34
- #
35
- # @author Sylvain Daubert
36
- class CP::RemoteService < CP::Base
37
-
38
- # @private
39
- @@event_sub_count = 0
40
-
41
- # Get event subscription count for all services
42
- # (unique ID for subscription)
43
- # @return [Integer]
44
- def self.event_sub_count
45
- @@event_sub_count += 1
46
- end
47
-
48
-
49
- # @private SOAP integer types
50
- INTEGER_TYPES = %w(ui1 ui2 ui4 i1 i2 i4 int).freeze
51
- # @private SOAP float types
52
- FLOAT_TYPES = %w(r4 r8 number float).freeze
53
- # @private SOAP string types
54
- STRING_TYPES = %w(char string uuid).freeze
55
- # @private SOAP true values
56
- TRUE_TYPES = %w(1 true yes).freeze
57
- # @private SOAP false values
58
- FALSE_TYPES = %w(0 false no).freeze
59
-
60
- # Get device to which this service belongs to
61
- # @return [Device]
62
- attr_reader :device
63
-
64
- # Get service type
65
- # @return [String]
66
- attr_reader :type
67
- # Get service id
68
- # @return [String]
69
- attr_reader :id
70
- # URL for service description
71
- # @return [String]
72
- attr_reader :scpd_url
73
- # URL for control
74
- # @return [String]
75
- attr_reader :control_url
76
- # URL for eventing
77
- # @return [String]
78
- attr_reader :event_sub_url
79
-
80
- # XML namespace for device description
81
- # @return [String]
82
- attr_reader :xmlns
83
- # Define architecture on which the service is implemented
84
- # @return [String]
85
- attr_reader :spec_version
86
- # Available actions on this service
87
- # @return [Array<Hash>]
88
- attr_reader :actions
89
- # State table for the service
90
- # @return [Array<Hash>]
91
- attr_reader :state_table
92
-
93
- # @param [Device] device
94
- # @param [String] url_base
95
- # @param [Hash] service
96
- def initialize(device, url_base, service)
97
- super()
98
- @device = device
99
- @description = service
100
-
101
- @type = service[:service_type].to_s
102
- @id = service[:service_id].to_s
103
- @scpd_url = build_url(url_base, service[:scpdurl].to_s)
104
- @control_url = build_url(url_base, service[:control_url].to_s)
105
- @event_sub_url = build_url(url_base, service[:event_sub_url].to_s)
106
- @actions = []
107
-
108
- initialize_savon
109
- end
110
64
 
111
- # Get service from its description
112
- # @return [void]
113
- def fetch
114
- scpd_getter = EM::DefaultDeferrable.new
65
+ # @private SOAP integer types
66
+ INTEGER_TYPES = %w(ui1 ui2 ui4 i1 i2 i4 int).freeze
67
+ # @private SOAP float types
68
+ FLOAT_TYPES = %w(r4 r8 number float).freeze
69
+ # @private SOAP string types
70
+ STRING_TYPES = %w(char string uuid).freeze
71
+ # @private SOAP true values
72
+ TRUE_TYPES = %w(1 true yes).freeze
73
+ # @private SOAP false values
74
+ FALSE_TYPES = %w(0 false no).freeze
75
+
76
+ # Get device to which this service belongs to
77
+ # @return [Device]
78
+ attr_reader :device
79
+
80
+ # Get service type
81
+ # @return [String]
82
+ attr_reader :type
83
+ # Get service id
84
+ # @return [String]
85
+ attr_reader :id
86
+ # URL for service description
87
+ # @return [String]
88
+ attr_reader :scpd_url
89
+ # URL for control
90
+ # @return [String]
91
+ attr_reader :control_url
92
+ # URL for eventing
93
+ # @return [String]
94
+ attr_reader :event_sub_url
95
+
96
+ # XML namespace for device description
97
+ # @return [String]
98
+ attr_reader :xmlns
99
+ # Define architecture on which the service is implemented
100
+ # @return [String]
101
+ attr_reader :spec_version
102
+ # Available actions on this service
103
+ # @return [Array<Hash>]
104
+ attr_reader :actions
105
+ # State table for the service
106
+ # @return [Array<Hash>]
107
+ attr_reader :state_table
108
+ # Variables from state table
109
+ # @return [Hash]
110
+ attr_reader :variables
111
+
112
+ # @param [Device] device
113
+ # @param [String] url_base
114
+ # @param [Hash] service
115
+ def initialize(device, url_base, service)
116
+ super()
117
+ @device = device
118
+ @description = service
119
+
120
+ @type = service[:service_type].to_s
121
+ @id = service[:service_id].to_s
122
+ @scpd_url = build_url(url_base, service[:scpdurl].to_s)
123
+ @control_url = build_url(url_base, service[:control_url].to_s)
124
+ @event_sub_url = build_url(url_base, service[:event_sub_url].to_s)
125
+ @actions = []
126
+ @variables = {}
127
+ @update_variables = EM::Channel.new
128
+
129
+ @update_variables.subscribe do |var, value|
130
+ @variables[var] = value
131
+ log :debug, "varibale #{var} set to #{value}"
132
+ end
115
133
 
116
- scpd_getter.errback do
117
- fail "cannot get SCPD from #@scpd_url"
118
- next
134
+ initialize_savon
119
135
  end
120
136
 
121
- scpd_getter.callback do |scpd|
122
- if bad_description?(scpd)
123
- fail 'not a UPNP 1.0/1.1 SCPD'
137
+ # Get service from its description
138
+ # @return [void]
139
+ def fetch
140
+ scpd_getter = EM::DefaultDeferrable.new
141
+
142
+ scpd_getter.errback do
143
+ fail "cannot get SCPD from #@scpd_url"
124
144
  next
125
145
  end
126
146
 
127
- extract_service_state_table scpd
128
- extract_actions scpd
147
+ scpd_getter.callback do |scpd|
148
+ if bad_description?(scpd)
149
+ fail 'not a UPNP 1.0/1.1 SCPD'
150
+ next
151
+ end
129
152
 
130
- succeed self
131
- end
153
+ extract_service_state_table scpd
154
+ extract_actions scpd
132
155
 
133
- get_description @scpd_url, scpd_getter
134
- end
156
+ succeed self
157
+ end
135
158
 
136
- # Subscribe to event
137
- # @param [Hash] options
138
- # @option options [Integer] timeout
139
- # @yieldparam [Event] event event received
140
- # @yieldparam [Object] msg message received
141
- # @return [Integer] subscribe id. May be used to unsubscribe on event
142
- def subscribe_to_event(options={}, &blk)
143
- cp = device.control_point
144
-
145
- cp.start_event_server
146
-
147
- port = cp.event_port
148
- num = self.class.event_sub_count
149
- @callback_url = "http://#{HOST_IP}:#{port}/event#{num}}"
150
-
151
- uri = URI(@event_sub_url)
152
- options[:timeout] ||= EVENT_SUB_DEFAULT_TIMEOUT
153
-
154
- log :info, "send SUBSCRIBE request to #{uri}"
155
- con = EM::HttpRequest.new(@event_sub_url)
156
- http = con.setup_request(:subscribe, :head => {
157
- 'HOST' => "#{uri.host}:#{uri.port}",
158
- 'USER-AGENT' => RUPNP::USER_AGENT,
159
- 'CALLBACK' => @callback_url,
160
- 'NT' => 'upnp:event',
161
- 'TIMEOUT' => "Second-#{options[:timeout]}"})
162
-
163
- http.errback do |client|
164
- log :warn, "Cannot subscribe to event: #{client.error}"
159
+ get_description @scpd_url, scpd_getter
165
160
  end
166
161
 
167
- http.callback do
168
- log :debug, 'Close connection to subscribe event URL'
169
- con.close
170
- if http.response_header.status != 200
171
- log :warn, "Cannot subscribe to event #@event_sub_url:" +
172
- " #{http.response_header.http_reason}"
173
- else
174
- timeout = http.response_header['TIMEOUT'].match(/(\d+)/)[1] || 1800
175
- event = Event.new(@event_sub_url, @callback_url,
176
- http.response_header['SID'], timeout.to_i)
177
- cp.add_event_url << ["/event#{num}", event]
178
- event.subscribe &blk
162
+ # Subscribe to event
163
+ # @param [Hash] options
164
+ # @option options [Integer] timeout
165
+ # @yieldparam [Event] event event received
166
+ # @yieldparam [Object] msg message received
167
+ # @return [Integer] subscribe id. May be used to unsubscribe on event
168
+ # @since 0.3.0
169
+ def subscribe_to_event(options={}, &blk)
170
+ cp = device.control_point
171
+
172
+ cp.start_event_server
173
+
174
+ port = cp.event_port
175
+ num = self.class.event_sub_count
176
+ @callback_url = "http://#{HOST_IP}:#{port}/events/#{num}"
177
+
178
+ uri = URI(@event_sub_url)
179
+ options[:timeout] ||= EVENT_SUB_DEFAULT_TIMEOUT
180
+
181
+ log :info, "send SUBSCRIBE request to #{uri}"
182
+ con = EM::HttpRequest.new(@event_sub_url)
183
+ http = con.setup_request(:subscribe, :head => {
184
+ 'HOST' => "#{uri.host}:#{uri.port}",
185
+ 'USER-AGENT' => RUPNP::USER_AGENT,
186
+ 'CALLBACK' => @callback_url,
187
+ 'NT' => 'upnp:event',
188
+ 'TIMEOUT' => "Second-#{options[:timeout]}"})
189
+
190
+ http.errback do |client|
191
+ log :warn, "Cannot subscribe to event: #{client.error}"
192
+ end
193
+
194
+ http.callback do
195
+ log :debug, 'Close connection to subscribe event URL'
196
+ con.close
197
+ if http.response_header.status != 200
198
+ log :warn, "Cannot subscribe to event #@event_sub_url:" +
199
+ " #{http.response_header.http_reason}"
200
+ else
201
+ timeout = http.response_header['TIMEOUT'].match(/(\d+)/)[1] || 1800
202
+ event = Event.new(@event_sub_url, URI(@callback_url).path,
203
+ http.response_header['SID'], timeout.to_i)
204
+ EventServer.add_event event
205
+ log :info, 'event subscribtion registered'
206
+ log :debug, "event: #{event.inspect}"
207
+
208
+ event.subscribe do |msg|
209
+ log :debug, "event #{event} received"
210
+ if msg.is_a? Hash and msg[:content].is_a? Hash
211
+ msg[:content].each do |k, v|
212
+ if @variables.has_key? k
213
+ log :info, "update evented variable #{k}"
214
+ type = get_data_type_from_table(k)
215
+ @update_variables << [k, get_value_from_type(type, v)]
216
+ end
217
+ end
218
+ end
219
+ log :debug, "call user block"
220
+ blk.call(msg) if blk
221
+ end
222
+ end
179
223
  end
180
224
  end
181
- end
182
225
 
183
226
 
184
- private
185
-
186
- def bad_description?(scpd)
187
- if scpd[:scpd]
188
- bd = false
189
- @xmlns = scpd[:scpd][:@xmlns]
190
- bd |= @xmlns != "urn:schemas-upnp-org:service-1-0"
191
- bd |= scpd[:scpd][:spec_version][:major].to_i != 1
192
- @spec_version = scpd[:scpd][:spec_version][:major] + '.'
193
- @spec_version += scpd[:scpd][:spec_version][:minor]
194
- bd |= !scpd[:scpd][:service_state_table]
195
- bd | scpd[:scpd][:service_state_table].empty?
196
- else
197
- true
227
+ private
228
+
229
+ def bad_description?(scpd)
230
+ if scpd[:scpd]
231
+ bd = false
232
+ @xmlns = scpd[:scpd][:@xmlns]
233
+ bd |= @xmlns != "urn:schemas-upnp-org:service-1-0"
234
+ bd |= scpd[:scpd][:spec_version][:major].to_i != 1
235
+ @spec_version = scpd[:scpd][:spec_version][:major] + '.'
236
+ @spec_version += scpd[:scpd][:spec_version][:minor]
237
+ bd |= !scpd[:scpd][:service_state_table]
238
+ bd | scpd[:scpd][:service_state_table].empty?
239
+ else
240
+ true
241
+ end
198
242
  end
199
- end
200
243
 
201
- def extract_service_state_table(scpd)
202
- if scpd[:scpd][:service_state_table][:state_variable]
203
- @state_table = scpd[:scpd][:service_state_table][:state_variable]
244
+ def extract_service_state_table(scpd)
245
+ if scpd[:scpd][:service_state_table][:state_variable]
246
+ @state_table = scpd[:scpd][:service_state_table][:state_variable]
247
+
248
+ if @state_table.is_a? Hash
249
+ @state_table = [@state_table]
250
+ end
204
251
 
205
- if @state_table.is_a? Hash
206
- @state_table = [@state_table]
252
+ @state_table.each do |var|
253
+ name = var[:name]
254
+ if INTEGER_TYPES.include? var[:data_type]
255
+ value = var[:default_value] ||
256
+ var[:allowed_value_range][:minimum] || 0
257
+ elsif FLOAT_TYPES.include? var[:data_type]
258
+ value = var[:default_value] ||
259
+ var[:allowed_value_range][:minimum] || 0
260
+ elsif STRING_TYPES.include? var[:data_type]
261
+ value = var[:default_value] ||
262
+ var[:allowed_value_list][:allowed_value].first || ''
263
+ else
264
+ value = nil
265
+ end
266
+
267
+ value = get_value_from_type(var[:data_type], value)
268
+ @update_variables << [name, value]
269
+ end
207
270
  end
208
271
  end
209
- end
210
272
 
211
- def extract_actions(scpd)
212
- if scpd[:scpd][:action_list] and scpd[:scpd][:action_list][:action]
213
- log :info, "extract actions for service #@type"
214
- @actions = scpd[:scpd][:action_list][:action]
215
- @actions = [@actions] unless @actions.is_a? Array
216
- @actions.each do |action|
217
- action[:arguments] = action[:argument_list][:argument]
218
- action.delete :argument_list
219
- define_method_from_action action
273
+ def extract_actions(scpd)
274
+ if scpd[:scpd][:action_list] and scpd[:scpd][:action_list][:action]
275
+ log :info, "extract actions for service #@type"
276
+ @actions = scpd[:scpd][:action_list][:action]
277
+ @actions = [@actions] unless @actions.is_a? Array
278
+ @actions.each do |action|
279
+ action[:arguments] = action[:argument_list][:argument]
280
+ action.delete :argument_list
281
+ define_method_from_action action
282
+ end
220
283
  end
221
284
  end
222
- end
223
285
 
224
- def define_method_from_action(action)
225
- action[:name] = action[:name].to_s
226
- action_name = action[:name]
227
- name = snake_case(action_name).to_sym
286
+ def define_method_from_action(action)
287
+ action[:name] = action[:name].to_s
288
+ action_name = action[:name]
289
+ name = snake_case(action_name).to_sym
228
290
 
229
- define_singleton_method(name) do |params|
230
- if params
231
- unless params.is_a? Hash
232
- raise ArgumentError, 'only hash arguments are accepted'
291
+ define_singleton_method(name) do |params|
292
+ if params
293
+ unless params.is_a? Hash
294
+ raise ArgumentError, 'only hash arguments are accepted'
295
+ end
233
296
  end
234
- end
235
- response = @soap.call(action_name) do |locals|
236
- locals.attributes 'xmlns:u' => @type
237
- locals.soap_action "#{type}##{action_name}"
238
- locals.message params
239
- end
240
-
241
- if action[:arguments].is_a? Hash
242
- log :debug, 'only one argument in argument list'
243
- if action[:arguments][:direction] == 'out'
244
- process_soap_response name, response, action[:arguments]
297
+ response = @soap.call(action_name) do |locals|
298
+ locals.attributes 'xmlns:u' => @type
299
+ locals.soap_action "#{type}##{action_name}"
300
+ locals.message params
245
301
  end
246
- else
247
- log :debug, 'true argument list'
248
- hsh = {}
249
- outer = action[:arguments].select { |arg| arg[:direction] == 'out' }
250
- outer.each do |arg|
251
- hsh.merge! process_soap_response(name, response, arg)
302
+
303
+ if action[:arguments].is_a? Hash
304
+ log :debug, 'only one argument in argument list'
305
+ if action[:arguments][:direction] == 'out'
306
+ process_soap_response name, response, action[:arguments]
307
+ end
308
+ else
309
+ log :debug, 'true argument list'
310
+ hsh = {}
311
+ outer = action[:arguments].select { |arg| arg[:direction] == 'out' }
312
+ outer.each do |arg|
313
+ hsh.merge! process_soap_response(name, response, arg)
314
+ end
315
+ hsh
252
316
  end
253
- hsh
254
317
  end
255
318
  end
256
- end
257
319
 
258
- def process_soap_response(action, resp, out_arg)
259
- if resp.success? and resp.to_xml.empty?
260
- log :debug, 'Successful SOAP request but empty response'
261
- return {}
320
+ def process_soap_response(action, resp, out_arg)
321
+ if resp.success? and resp.to_xml.empty?
322
+ log :debug, 'Successful SOAP request but empty response'
323
+ return {}
324
+ end
325
+
326
+ state_var = @state_table.find do |h|
327
+ h[:name] == out_arg[:related_state_variable]
328
+ end
329
+
330
+ action_response = "#{action}_response".to_sym
331
+ out_arg_name = snake_case(out_arg[:name]).to_sym
332
+ value = resp.hash[:envelope][:body][action_response][out_arg_name]
333
+
334
+ { out_arg_name => get_value_from_type(state_var[:data_type], value) }
262
335
  end
263
336
 
264
- state_var = @state_table.find do |h|
265
- h[:name] == out_arg[:related_state_variable]
337
+ def get_value_from_type(type, value)
338
+ transform_method = if INTEGER_TYPES.include? type
339
+ :to_i
340
+ elsif FLOAT_TYPES.include? type
341
+ :to_f
342
+ elsif STRING_TYPES.include? type
343
+ :to_s
344
+ end
345
+ if transform_method
346
+ value.send(transform_method)
347
+ elsif TRUE_TYPES.include? type
348
+ true
349
+ elsif FALSE_TYPES.include? type
350
+ false
351
+ else
352
+ log :warn, "SOAP response has an unknown type: #{type}"
353
+ nil
354
+ end
266
355
  end
267
356
 
268
- action_response = "#{action}_response".to_sym
269
- out_arg_name = snake_case(out_arg[:name]).to_sym
270
- value = resp.hash[:envelope][:body][action_response][out_arg_name]
271
-
272
- transform_method = if INTEGER_TYPES.include? state_var[:data_type]
273
- :to_i
274
- elsif FLOAT_TYPES.include? state_var[:data_type]
275
- :to_f
276
- elsif STRING_TYPES.include? state_var[:data_type]
277
- :to_s
278
- end
279
- if transform_method
280
- { out_arg_name => value.send(transform_method) }
281
- elsif TRUE_TYPES.include? state_var[:data_type]
282
- { out_arg_name => true }
283
- elsif FALSE_TYPES.include? state_var[:data_type]
284
- { out_arg_name => false }
285
- else
286
- log :warn, "SOAP response has an unknown type: #{state_var[:data_type]}"
287
- {}
357
+ def get_data_type_from_table(var_name)
358
+ @state_table.find { |v| v[:name] == var_name }[:data_type]
288
359
  end
289
- end
290
360
 
291
- def initialize_savon
292
- @soap = Savon.client do |globals|
293
- globals.log_level :error
294
- globals.endpoint @control_url
295
- globals.namespace @type
296
- globals.convert_request_keys_to :camel_case
297
- globals.log true
298
- globals.headers :HOST => "#{HOST_IP}"
299
- globals.env_namespace 's'
300
- globals.namespace_identifier 'u'
361
+ def initialize_savon
362
+ @soap = Savon.client do |globals|
363
+ globals.log_level :error
364
+ globals.endpoint @control_url
365
+ globals.namespace @type
366
+ globals.convert_request_keys_to :camel_case
367
+ globals.log true
368
+ globals.headers :HOST => "#{HOST_IP}"
369
+ globals.env_namespace 's'
370
+ globals.namespace_identifier 'u'
371
+ end
301
372
  end
373
+
302
374
  end
303
375
 
304
376
  end
305
-
306
377
  end
@@ -8,6 +8,7 @@ module RUPNP
8
8
  # Get service ID
9
9
  # @return [Integer]
10
10
  attr_reader :sid
11
+ attr_reader :callback_url
11
12
 
12
13
  # @param [String] event_suburl Event subscription URL
13
14
  # @param [String] callback_url Callback URL to receive events
@@ -16,17 +17,20 @@ module RUPNP
16
17
  def initialize(event_suburl, callback_url, sid, timeout)
17
18
  super()
18
19
  @event_suburl = event_suburl
20
+ @callback_url = callback_url
19
21
  @sid, @timeout = sid, timeout
20
22
 
21
23
  @timeout_timer = EM.add_timer(@timeout) { self << :timeout }
22
24
  end
23
25
 
24
26
  # Renew subscription to event
27
+ # @todo
25
28
  def renew_subscription
26
29
  raise NotImplementedError
27
30
  end
28
31
 
29
32
  # Cancel subscription to event
33
+ # @todo
30
34
  def cancel_subscription
31
35
  raise NotImplementedError
32
36
  end
@@ -0,0 +1,140 @@
1
+ require_relative '../spec_helper'
2
+
3
+ def start_server
4
+ EM.start_server('127.0.0.1', RUPNP::EVENT_SUB_DEFAULT_PORT,
5
+ RUPNP::CP::EventServer)
6
+ end
7
+
8
+
9
+ module RUPNP
10
+ module CP
11
+
12
+ describe EventServer do
13
+ include EM::SpecHelper
14
+
15
+ let(:timeout) { 2 }
16
+ let(:sid) { "uuid:#{UUID.generate}" }
17
+ let(:event_uri) { '/event/1' }
18
+ let(:event) { Event.new('', event_uri, sid, timeout) }
19
+ let(:port) { RUPNP::EVENT_SUB_DEFAULT_PORT }
20
+ let(:req) {EM::HttpRequest.new("http://127.0.0.1:#{port}#{event_uri}")}
21
+
22
+ it 'should return 404 error on bad HTTP method URI' do
23
+ em do
24
+ start_server
25
+
26
+ req = EM::HttpRequest.new("http://127.0.0.1:#{port}/unknown")
27
+ http = send_notify_request(req)
28
+ http.errback { fail 'must not fail!' }
29
+ http.callback do
30
+ expect(http.response_header.status).to eq(404)
31
+ done
32
+ end
33
+ end
34
+ end
35
+
36
+ it 'should return 405 error on bad HTTP method' do
37
+ em do
38
+ start_server
39
+
40
+ http = req.get
41
+ http.callback do
42
+ expect(http.response_header.status).to eq(405)
43
+ done
44
+ end
45
+ end
46
+ end
47
+
48
+ it 'should return 400 error on malformed request' do
49
+ em do
50
+ req2 = req.dup
51
+ EventServer.add_event event
52
+ start_server
53
+
54
+ http = send_notify_request(req, :delete => 'NT')
55
+ http.errback { fail 'must not fail!' }
56
+ http.callback do
57
+ expect(http.response_header.status).to eq(400)
58
+ http2 = send_notify_request(req2, :delete => 'NTS')
59
+ http2.callback do
60
+ expect(http2.response_header.status).to eq(400)
61
+ done
62
+ end
63
+ end
64
+ end
65
+ EventServer.remove_event event
66
+ end
67
+
68
+ it 'should return 412 error on bad request' do
69
+ em do
70
+ EventServer.add_event event
71
+ start_server
72
+ http = send_notify_request(req, 'SID' => "uuid:#{UUID.generate}")
73
+ http.errback { fail 'must not fail!' }
74
+ http.callback do
75
+ expect(http.response_header.status).to eq(412)
76
+ done
77
+ end
78
+ end
79
+
80
+ em do
81
+ start_server
82
+ http = send_notify_request(req, 'NT' => "upnp:other")
83
+ http.errback { fail 'must not fail!' }
84
+ http.callback do
85
+ expect(http.response_header.status).to eq(412)
86
+ done
87
+ end
88
+ end
89
+
90
+
91
+ em do
92
+ start_server
93
+ http = send_notify_request(req, 'NTS' => "upnp:other")
94
+ http.errback { fail 'must not fail!' }
95
+ http.callback do
96
+ expect(http.response_header.status).to eq(412)
97
+ done
98
+ end
99
+ end
100
+
101
+ em do
102
+ start_server
103
+ http = send_notify_request(req, :delete => 'SID')
104
+ http.errback { fail 'must not fail!' }
105
+ http.callback do
106
+ expect(http.response_header.status).to eq(412)
107
+ done
108
+ end
109
+ end
110
+ EventServer.remove_event event
111
+ end
112
+
113
+ it 'should receive a NOTIFY request' do
114
+ em do
115
+ EventServer.add_event event
116
+ start_server
117
+ http = send_notify_request(req, 'SID' => sid)
118
+ http.errback { fail 'must not fail!' }
119
+ http_ok = event_ok = false
120
+ http.callback do
121
+ expect(http.response_header.status).to eq(200)
122
+ http_ok = true
123
+ done if event_ok
124
+ end
125
+ event.subscribe do |h|
126
+ expect(h[:seq]).to be_a(Integer)
127
+ expect(h[:content]).to be_a(Hash)
128
+ event_ok = true
129
+ done if http_ok
130
+ end
131
+ end
132
+ EventServer.remove_event event
133
+ end
134
+
135
+ it "should return updated variables through 'events' channel"
136
+ it 'should serve multiple URLs'
137
+ end
138
+
139
+ end
140
+ end
@@ -37,12 +37,19 @@ module RUPNP
37
37
  em do
38
38
  rs.errback { fail 'RemoteService#fetch should work' }
39
39
  rs.callback do
40
- expect(rs.state_table).to have(4).items
40
+ expect(rs.state_table).to have(5).items
41
41
  expect(rs.state_table[0][:name]).to eq('X_variableName1')
42
42
  expect(rs.state_table[1][:data_type]).to eq('ui4')
43
43
  expect(rs.state_table[2][:default_value]).to eq('2')
44
44
  expect(rs.state_table[3][:allowed_value_range][:maximum]).
45
45
  to eq('255')
46
+ expect(rs.state_table[4][:data_type]).to eq('string')
47
+ expect(rs.state_table[4][:default_value]).to eq('none')
48
+
49
+ expect(rs.variables['X_variableName1']).to be_a(Fixnum)
50
+ 4.times do |i|
51
+ expect(rs.variables["X_variableName#{i+1}"]).to eq(i)
52
+ end
46
53
  done
47
54
  end
48
55
  rs.fetch
@@ -147,14 +154,19 @@ module RUPNP
147
154
  em do
148
155
  rs.errback { fail 'RemoteService#fetch should work' }
149
156
  rs.callback do
157
+ expect(rs.variables['X_variableName1']).to eq(0)
150
158
  rs.subscribe_to_event do |msg|
159
+ expect(rs.variables['X_variableName1']).to eq(12)
151
160
  done
152
161
  end
153
162
  end
154
163
  rs.fetch
155
164
 
156
- rs.device.control_point.add_event_url.subscribe do |url, event|
157
- EM.add_timer(0) { event << 'message' }
165
+ EM.add_timer(1) do
166
+ event = class EventServer; @@events.last; end
167
+ url = "http://127.0.0.1:8080#{event.callback_url}"
168
+ conn = EM::HttpRequest.new(url)
169
+ send_notify_request(conn, 'SID' => event.sid)
158
170
  end
159
171
  end
160
172
  end
@@ -10,6 +10,7 @@ require 'webmock/rspec'
10
10
 
11
11
  RUPNP.log_level = :failure
12
12
 
13
+ WebMock.disable_net_connect!(allow_localhost: true)
13
14
 
14
15
  class FakeMulticast < RUPNP::SSDP::MulticastConnection
15
16
  attr_reader :handshake_response, :packets
@@ -151,6 +152,17 @@ EOAL
151
152
  </stateVariable>
152
153
  EOSV
153
154
  end
155
+ scpd << <<EOSV2
156
+ <stateVariable sendEvents="#{opt[:send_event] ? 'yes' : 'no'}">
157
+ <name>X_variableStr</name>
158
+ <dataType>string</dataType>
159
+ <defaultValue>none</defaultValue>
160
+ <allowedValueList>
161
+ <allowedValue>none</allowedValue>
162
+ <allowedValue>another value</allowedValue>
163
+ </allowedValueList>
164
+ </stateVariable>
165
+ EOSV2
154
166
  scpd << " </serviceStateTable>\n</scpd>\n"
155
167
  end
156
168
 
@@ -175,6 +187,34 @@ EOD
175
187
  end
176
188
 
177
189
 
190
+ def event_body
191
+ <<EOD
192
+ <?xml version="1.0"?>
193
+ <e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
194
+ <e:property>
195
+ <X_variableName1>12</variableName>
196
+ </e:property>
197
+ </e:propertyset>
198
+ EOD
199
+ end
200
+
201
+
202
+ def send_notify_request(req, options={})
203
+ delete = options.delete(:delete)
204
+ headers = {
205
+ 'HOST' => "127.0.0.1:1234",
206
+ 'USER-AGENT' => RUPNP::USER_AGENT,
207
+ 'CONTENT-TYPE' => 'text/xml; charset="utf-8"',
208
+ 'NT' => 'upnp:event',
209
+ 'NTS' => 'upnp:propchange',
210
+ 'SID' => "uuid:#{UUID.generate}",
211
+ 'SEQ' => 0 }.merge(options)
212
+ headers.delete delete if delete
213
+
214
+ req.setup_request(:notify, :head => headers, :body => event_body)
215
+ end
216
+
217
+
178
218
  NOTIFY_REGEX = {
179
219
  :common => [
180
220
  /^NOTIFY \* HTTP\/1.1\r\n/,
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rupnp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-02-01 00:00:00.000000000 Z
12
+ date: 2014-02-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: uuid
@@ -187,6 +187,7 @@ files:
187
187
  - spec/ssdp/searcher_spec.rb
188
188
  - spec/cp/remote_service_spec.rb
189
189
  - spec/cp/remote_device_spec.rb
190
+ - spec/cp/event_server_spec.rb
190
191
  - spec/control_point_spec.rb
191
192
  - spec/spec_helper.rb
192
193
  - lib/rupnp.rb
@@ -203,7 +204,6 @@ files:
203
204
  - lib/rupnp/cp/base.rb
204
205
  - lib/rupnp/cp/event_server.rb
205
206
  - lib/rupnp/cp/remote_device.rb
206
- - lib/rupnp/cp/event_subscriber.rb
207
207
  - lib/rupnp/discover.rb
208
208
  - lib/rupnp/ssdp.rb
209
209
  - lib/rupnp/log_mixin.rb
@@ -1,47 +0,0 @@
1
- module RUPNP
2
-
3
- # Event subscriber to an event's service
4
- # @author Sylvain Daubert
5
- class CP::EventSubscriber < EM::Connection
6
- include LogMixin
7
-
8
- # Response from device
9
- # @return [EM::Channel]
10
- attr_reader :response
11
-
12
-
13
- # @param [String] msg message to send for subscribing
14
- def initialize(msg)
15
- @msg = msg
16
- @response = EM::Channel.new
17
- end
18
-
19
- # @return [void]
20
- def post_init
21
- log :debug, "send event subscribe request:\n#@msg"
22
- send_data @msg
23
- end
24
-
25
- # Receive response from device and send it through {#response}
26
- # @param [String] data
27
- # @return [void]
28
- def receive_data(data)
29
- log :debug, "receive data from subscribe event action:\n#{data}"
30
- resp = {}
31
- io = StringIO.new(data)
32
-
33
- status = io.readline
34
-
35
- if status =~ /HTTP\/1\.1 (\d+) (.+)/
36
- resp[:status] = $2
37
- resp[:status_code] = $1
38
-
39
- resp.merge!(get_http_headers(io))
40
-
41
- @response << resp
42
- end
43
- end
44
-
45
- end
46
-
47
- end