rupnp 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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