rsmp 0.1.13 → 0.1.29

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.
@@ -0,0 +1,206 @@
1
+ # waiting for various types of messages and reponses from remote sites
2
+ module RSMP
3
+ module SiteProxyWait
4
+
5
+ def wait_for_status_updates parent_task, options={}, &send_block
6
+ send_while_collecting parent_task, send_block do |task, m_id|
7
+ collect_status_updates_or_responses task, 'StatusUpdate', options, m_id
8
+ end
9
+ end
10
+
11
+ def wait_for_status_responses parent_task, options={}, &send_block
12
+ send_while_collecting parent_task, send_block do |task, m_id|
13
+ collect_status_updates_or_responses task, 'StatusResponse', options, m_id
14
+ end
15
+ end
16
+
17
+ def wait_for_command_responses parent_task, options={}, &send_block
18
+ send_while_collecting parent_task, send_block do |task, m_id|
19
+ collect_command_responses task, options, m_id
20
+ end
21
+ end
22
+
23
+ def wait_for_alarm parent_task, options={}
24
+ matching_alarm = nil
25
+ message = collect(parent_task,options.merge(type: "Alarm", with_message: true, num: 1)) do |message|
26
+ # TODO check components
27
+ matching_alarm = nil
28
+ alarm = message
29
+ next if options[:aCId] && options[:aCId] != alarm.attribute("aCId")
30
+ next if options[:aSp] && options[:aSp] != alarm.attribute("aSp")
31
+ next if options[:aS] && options[:aS] != alarm.attribute("aS")
32
+ matching_alarm = alarm
33
+ break
34
+ end
35
+ if item
36
+ { message: message, status: matching_alarm }
37
+ end
38
+ end
39
+
40
+ def collect_status_updates task, options, m_id
41
+ collect_status_updates_or_responses task, 'StatusUpdate', options, m_id
42
+ end
43
+
44
+ def collect_status_responses task, options, m_id
45
+ collect_status_updates_or_responses task, 'StatusResponse', options, m_id
46
+ end
47
+
48
+ def collect_command_responses parent_task, options, m_id
49
+ task.annotate "wait for command response"
50
+ want = options[:command_list].clone
51
+ result = {}
52
+ messages = []
53
+ collect(parent_task,options.merge({
54
+ type: ['CommandResponse','MessageNotAck'],
55
+ num: 1
56
+ })) do |message|
57
+ if message.is_a?(MessageNotAck)
58
+ if message.attribute('oMId') == m_id
59
+ # set result to an exception, but don't raise it.
60
+ # this will be returned by the task and stored as the task result
61
+ # when the parent task call wait() on the task, the exception
62
+ # will be raised in the parent task, and caught by rspec.
63
+ # rspec will then show the error and record the test as failed
64
+ m_id_short = RSMP::Message.shorten_m_id m_id, 8
65
+ result = RSMP::MessageRejected.new "Command request #{m_id_short} was rejected: #{message.attribute('rea')}"
66
+ next true # done, no more messages wanted
67
+ else
68
+ false
69
+ end
70
+ else
71
+ add = false
72
+ # look through querues
73
+ want.each_with_index do |query,i|
74
+ # look through items in message
75
+ message.attributes['rvs'].each do |input|
76
+ matching = command_match? query, input
77
+ if matching == true
78
+ result[query] = input
79
+ add = true
80
+ elsif matching == false
81
+ result.delete query
82
+ end
83
+ end
84
+ end
85
+ messages << message if add
86
+ result.size == want.size # any queries left to match?
87
+ end
88
+ end
89
+ return result, messages
90
+ rescue Async::TimeoutError
91
+ raise RSMP::TimeoutError.new "Did not receive correct command response to #{m_id} within #{options[:timeout]}s"
92
+ end
93
+
94
+ def collect_status_updates_or_responses task, type, options, m_id
95
+ want = options[:status_list].clone
96
+ result = {}
97
+ messages = []
98
+ # wait for a status update
99
+ collect(task,options.merge({
100
+ type: [type,'MessageNotAck'],
101
+ num: 1
102
+ })) do |message|
103
+ if message.is_a?(MessageNotAck)
104
+ if message.attribute('oMId') == m_id
105
+ # set result to an exception, but don't raise it.
106
+ # this will be returned by the task and stored as the task result
107
+ # when the parent task call wait() on the task, the exception
108
+ # will be raised in the parent task, and caught by rspec.
109
+ # rspec will then show the error and record the test as failed
110
+ m_id_short = RSMP::Message.shorten_m_id m_id, 8
111
+ result = RSMP::MessageRejected.new "Status request #{m_id_short} was rejected: #{message.attribute('rea')}"
112
+ next true # done, no more messages wanted
113
+ end
114
+ false
115
+ else
116
+ found = []
117
+ add = false
118
+ # look through querues
119
+ want.each_with_index do |query,i|
120
+ # look through status items in message
121
+ message.attributes['sS'].each do |input|
122
+ matching = status_match? query, input
123
+ if matching == true
124
+ result[query] = input
125
+ add = true
126
+ elsif matching == false
127
+ result.delete query
128
+ end
129
+ end
130
+ end
131
+ messages << message if add
132
+ result.size == want.size # any queries left to match?
133
+ end
134
+ end
135
+ return result, messages
136
+ rescue Async::TimeoutError
137
+ type_str = {'StatusUpdate'=>'update', 'StatusResponse'=>'response'}[type]
138
+ raise RSMP::TimeoutError.new "Did not received correct status #{type_str} in reply to #{m_id} within #{options[:timeout]}s"
139
+ end
140
+
141
+ def status_match? query, item
142
+ return nil if query['sCI'] && query['sCI'] != item['sCI']
143
+ return nil if query['n'] && query['n'] != item['n']
144
+ return false if query['q'] && query['q'] != item['q']
145
+ if query['s'].is_a? Regexp
146
+ return false if query['s'] && item['s'] !~ query['s']
147
+ else
148
+ return false if query['s'] && item['s'] != query['s']
149
+ end
150
+ true
151
+ end
152
+
153
+ def command_match? query, item
154
+ return nil if query['cCI'] && query['cCI'] != item['cCI']
155
+ return nil if query['n'] && query['n'] != item['n']
156
+ if query['v'].is_a? Regexp
157
+ return false if query['v'] && item['v'] !~ query['v']
158
+ else
159
+ return false if query['v'] && item['v'] != query['v']
160
+ end
161
+ true
162
+ end
163
+
164
+ def send_while_collecting parent_task, send_block, &collect_block
165
+ m_id = RSMP::Message.make_m_id # make message id so we can start waiting for it
166
+
167
+ # wait for command responses in an async task
168
+ task = parent_task.async do |task|
169
+ collect_block.call task, m_id
170
+ rescue StandardError => e
171
+ notify_error e, level: :internal
172
+ end
173
+
174
+ # call block, it should send command request using the given m_id
175
+ send_block.call m_id
176
+
177
+ # wait for the response and return it, raise exception if NotAck received, it it timed out
178
+ task.wait
179
+ end
180
+
181
+ def wait_for_aggregated_status parent_task, options={}
182
+ collect(parent_task,options.merge({
183
+ type: ['AggregatedStatus','MessageNotAck'],
184
+ num: 1
185
+ })) do |message|
186
+ if message.is_a?(MessageNotAck)
187
+ if message.attribute('oMId') == m_id
188
+ # set result to an exception, but don't raise it.
189
+ # this will be returned by the task and stored as the task result
190
+ # when the parent task call wait() on the task, the exception
191
+ # will be raised in the parent task, and caught by rspec.
192
+ # rspec will then show the error and record the test as failed
193
+ m_id_short = RSMP::Message.shorten_m_id m_id, 8
194
+ result = RSMP::MessageRejected.new "Aggregated status request #{m_id_short} was rejected: #{message.attribute('rea')}"
195
+ next true # done, no more messages wanted
196
+ else
197
+ false
198
+ end
199
+ else
200
+ true
201
+ end
202
+ end
203
+ end
204
+
205
+ end
206
+ end
@@ -17,47 +17,57 @@ module RSMP
17
17
  @supervisor_settings['site_id']
18
18
  end
19
19
 
20
- def handle_supervisor_settings options
21
- @supervisor_settings = {
20
+ def handle_supervisor_settings options={}
21
+ defaults = {
22
22
  'port' => 12111,
23
- 'rsmp_versions' => ['3.1.1','3.1.2','3.1.3','3.1.4'],
24
- 'timer_interval' => 0.1,
25
- 'watchdog_interval' => 1,
26
- 'watchdog_timeout' => 2,
27
- 'acknowledgement_timeout' => 2,
28
- 'command_response_timeout' => 1,
29
- 'status_response_timeout' => 1,
30
- 'status_update_timeout' => 1,
31
- 'site_connect_timeout' => 2,
32
- 'site_ready_timeout' => 1,
33
- 'stop_after_first_session' => false,
34
- 'sites' => {
35
- :any => {}
23
+ 'ips' => 'all',
24
+ 'guest' => {
25
+ 'rsmp_versions' => 'all',
26
+ 'sxl' => 'tlc',
27
+ 'intervals' => {
28
+ 'timer' => 1,
29
+ 'watchdog' => 1
30
+ },
31
+ 'timeouts' => {
32
+ 'watchdog' => 2,
33
+ 'acknowledgement' => 2
34
+ }
36
35
  }
37
36
  }
38
-
39
- if options[:supervisor_settings]
40
- converted = options[:supervisor_settings].map { |k,v| [k.to_s,v] }.to_h #convert symbol keys to string keys
41
- converted.compact!
42
- @supervisor_settings.merge! converted
43
- end
44
-
45
- required = [:port, :rsmp_versions, :watchdog_interval, :watchdog_timeout,
46
- :acknowledgement_timeout, :command_response_timeout]
47
- check_required_settings @supervisor_settings, required
48
37
 
38
+ # merge options into defaults
39
+ @supervisor_settings = defaults.deep_merge(options[:supervisor_settings] || {})
49
40
  @rsmp_versions = @supervisor_settings["rsmp_versions"]
41
+ check_site_sxl_types
42
+ end
43
+
44
+ def check_site_sxl_types
45
+ sites = @supervisor_settings['sites'].clone || {}
46
+ sites['guest'] = @supervisor_settings['guest']
47
+ sites.each do |site_id,settings|
48
+ unless settings
49
+ raise RSMP::ConfigurationError.new("Configuration for site '#{site_id}' is empty")
50
+ end
51
+ sxl = settings['sxl']
52
+ sxl = 'tlc' unless sxl # temporary fix until configs are updated
53
+ unless sxl
54
+ raise RSMP::ConfigurationError.new("Configuration error for site '#{site_id}': No SXL specified")
55
+ end
56
+ RSMP::Schemer.find_schemas! sxl if sxl
57
+ rescue RSMP::Schemer::UnknownSchemaError => e
58
+ raise RSMP::ConfigurationError.new("Configuration error for site '#{site_id}': #{e}")
59
+ end
50
60
  end
51
61
 
52
62
  def start_action
53
63
  @endpoint = Async::IO::Endpoint.tcp('0.0.0.0', @supervisor_settings["port"])
54
- @endpoint.accept do |socket|
64
+ @endpoint.accept do |socket| # creates async tasks
55
65
  handle_connection(socket)
66
+ rescue StandardError => e
67
+ notify_error e, level: :internal
56
68
  end
57
- rescue SystemCallError => e # all ERRNO errors
58
- log "Exception: #{e.to_s}", level: :error
59
69
  rescue StandardError => e
60
- log ["Exception: #{e.inspect}",e.backtrace].flatten.join("\n"), level: :error
70
+ notify_error e, level: :internal
61
71
  end
62
72
 
63
73
  def stop
@@ -74,16 +84,18 @@ module RSMP
74
84
  remote_hostname = socket.remote_address.ip_address
75
85
  remote_ip = socket.remote_address.ip_address
76
86
 
77
- info = {ip:remote_ip, port:remote_port, hostname:remote_hostname, now:RSMP.now_string()}
87
+ info = {ip:remote_ip, port:remote_port, hostname:remote_hostname, now:Clock.now}
78
88
  if accept? socket, info
79
89
  connect socket, info
80
90
  else
81
91
  reject socket, info
82
92
  end
83
- rescue SystemCallError => e # all ERRNO errors
84
- log "Exception: #{e.to_s}", level: :error
93
+ rescue ConnectionError => e
94
+ log "Rejected connection from #{remote_ip}:#{remote_port}, #{e.to_s}", level: :warning
95
+ notify_error e
85
96
  rescue StandardError => e
86
- log "Exception: #{e}", exception: e, level: :error
97
+ log "Connection: #{e.to_s}", exception: e, level: :error
98
+ notify_error e, level: :internal
87
99
  ensure
88
100
  close socket, info
89
101
  end
@@ -91,7 +103,7 @@ module RSMP
91
103
  def starting
92
104
  log "Starting supervisor on port #{@supervisor_settings["port"]}",
93
105
  level: :info,
94
- timestamp: RSMP.now_object
106
+ timestamp: @clock.now
95
107
  end
96
108
 
97
109
  def accept? socket, info
@@ -110,29 +122,49 @@ module RSMP
110
122
  end
111
123
  end
112
124
 
125
+ def authorize_ip ip
126
+ return if @supervisor_settings['ips'] == 'all'
127
+ return if @supervisor_settings['ips'].include? ip
128
+ raise ConnectionError.new('guest ip not allowed')
129
+ end
130
+
131
+ def check_max_sites
132
+ max = @supervisor_settings['max_sites']
133
+ if max
134
+ if @proxies.size >= max
135
+ raise ConnectionError.new("maximum of #{max} sites already connected")
136
+ end
137
+ end
138
+ end
139
+
113
140
  def connect socket, info
114
141
  log "Site connected from #{format_ip_and_port(info)}",
115
142
  ip: info[:ip],
116
143
  port: info[:port],
117
144
  level: :info,
118
- timestamp: RSMP.now_object
145
+ timestamp: Clock.now
146
+
147
+ authorize_ip info[:ip]
148
+ check_max_sites
119
149
 
120
150
  proxy = build_proxy({
121
151
  supervisor: self,
152
+ ip: info[:ip],
153
+ port: info[:port],
122
154
  task: @task,
123
- settings: @supervisor_settings[:sites],
155
+ settings: {'collect'=>@supervisor_settings['collect']},
124
156
  socket: socket,
125
157
  info: info,
126
158
  logger: @logger,
127
159
  archive: @archive
128
160
  })
129
161
  @proxies.push proxy
130
-
131
162
  proxy.run # will run until the site disconnects
163
+ ensure
132
164
  @proxies.delete proxy
133
165
  site_ids_changed
134
166
 
135
- stop if @supervisor_settings['stop_after_first_session']
167
+ stop if @supervisor_settings['one_shot']
136
168
  end
137
169
 
138
170
  def site_ids_changed
@@ -145,9 +177,9 @@ module RSMP
145
177
 
146
178
  def close socket, info
147
179
  if info
148
- log "Connection to #{format_ip_and_port(info)} closed", ip: info[:ip], level: :info, timestamp: RSMP.now_object
180
+ log "Connection to #{format_ip_and_port(info)} closed", ip: info[:ip], level: :info, timestamp: Clock.now
149
181
  else
150
- log "Connection closed", level: :info, timestamp: RSMP.now_object
182
+ log "Connection closed", level: :info, timestamp: Clock.now
151
183
  end
152
184
 
153
185
  socket.close
@@ -167,34 +199,38 @@ module RSMP
167
199
  def wait_for_site site_id, timeout
168
200
  site = find_site site_id
169
201
  return site if site
170
- RSMP::Wait.wait_for(@task,@site_id_condition,timeout) { find_site site_id }
202
+ wait_for(@site_id_condition,timeout) { find_site site_id }
171
203
  rescue Async::TimeoutError
172
- nil
204
+ raise RSMP::TimeoutError.new "Site '#{site_id}' did not connect within #{timeout}s"
173
205
  end
174
206
 
175
207
  def wait_for_site_disconnect site_id, timeout
176
- RSMP::Wait.wait_for(@task,@site_id_condition,timeout) { true unless find_site site_id }
208
+ wait_for(@site_id_condition,timeout) { true unless find_site site_id }
177
209
  rescue Async::TimeoutError
178
- false
210
+ raise RSMP::TimeoutError.new "Site '#{site_id}' did not disconnect within #{timeout}s"
179
211
  end
180
212
 
181
213
  def check_site_id site_id
182
214
  check_site_already_connected site_id
183
- return find_allowed_site_setting site_id
215
+ return site_id_to_site_setting site_id
184
216
  end
185
217
 
186
218
  def check_site_already_connected site_id
187
- raise FatalError.new "Site #{site_id} already connected" if find_site(site_id)
219
+ raise FatalError.new "Site '#{site_id}' already connected" if find_site(site_id)
188
220
  end
189
221
 
190
- def find_allowed_site_setting site_id
222
+ def site_id_to_site_setting site_id
191
223
  return {} unless @supervisor_settings['sites']
192
224
  @supervisor_settings['sites'].each_pair do |id,settings|
193
- if id == :any || id == site_id
225
+ if id == 'guest' || id == site_id
194
226
  return settings
195
227
  end
196
228
  end
197
- raise FatalError.new "site id #{site_id} rejected"
229
+ raise FatalError.new "site id #{site_id} unknown"
230
+ end
231
+
232
+ def ip_to_site_settings ip
233
+ @supervisor_settings['sites'][ip] || @supervisor_settings['sites']['guest']
198
234
  end
199
235
 
200
236
  def aggregated_status_changed site_proxy, component
@@ -14,7 +14,6 @@ module RSMP
14
14
  @ip = options[:ip]
15
15
  @port = options[:port]
16
16
  @status_subscriptions = {}
17
- @status_subscriptions_mutex = Mutex.new
18
17
  @sxl = @site_settings['sxl']
19
18
  @synthetic_id = Supervisor.build_id_from_ip_port @ip, @port
20
19
  end
@@ -28,27 +27,34 @@ module RSMP
28
27
  super
29
28
  connect
30
29
  @logger.unmute @ip, @port
30
+ log "Connected to superviser at #{@ip}:#{@port}", level: :info
31
31
  start_reader
32
32
  send_version @site_settings['site_id'], @site_settings["rsmp_versions"]
33
33
  rescue Errno::ECONNREFUSED
34
34
  log "No connection to supervisor at #{@ip}:#{@port}", level: :error
35
- unless @site.site_settings["reconnect_interval"] == :no
36
- log "Will try to reconnect again every #{@site.site_settings["reconnect_interval"]} seconds..", level: :info
35
+ unless @site.site_settings['intervals']['reconnect'] == :no
36
+ log "Will try to reconnect again every #{@site.site_settings['intervals']['reconnect']} seconds..", level: :info
37
37
  @logger.mute @ip, @port
38
38
  end
39
39
  end
40
40
 
41
+ def stop
42
+ log "Closing connection to supervisor", level: :info
43
+ super
44
+ @last_status_sent = nil
45
+ end
46
+
41
47
  def connect
42
48
  return if @socket
43
49
  @endpoint = Async::IO::Endpoint.tcp(@ip, @port)
44
50
  @socket = @endpoint.connect
45
51
  @stream = Async::IO::Stream.new(@socket)
46
- @protocol = Async::IO::Protocol::Line.new(@stream,RSMP::WRAPPING_DELIMITER) # rsmp messages are json terminated with a form-feed
52
+ @protocol = Async::IO::Protocol::Line.new(@stream,WRAPPING_DELIMITER) # rsmp messages are json terminated with a form-feed
47
53
  end
48
54
 
49
55
  def connection_complete
50
56
  super
51
- log "Connection to supervisor established", level: :info
57
+ log "Connection to supervisor established, using core #{@rsmp_version}, #{sxl} #{sxl_version}", level: :info
52
58
  start_watchdog
53
59
  end
54
60
 
@@ -59,6 +65,8 @@ module RSMP
59
65
  when StatusUpdate
60
66
  when AggregatedStatus
61
67
  will_not_handle message
68
+ when AggregatedStatusRequest
69
+ process_aggregated_status_request message
62
70
  when CommandRequest
63
71
  process_command_request message
64
72
  when CommandResponse
@@ -72,14 +80,15 @@ module RSMP
72
80
  else
73
81
  super message
74
82
  end
75
- rescue UnknownComponent => e
76
- dont_acknowledge message, '', e.to_s
77
- rescue UnknownCommand => e
78
- dont_acknowledge message, '', e.to_s
79
- rescue UnknownStatus => e
83
+ rescue UnknownComponent, UnknownCommand, UnknownStatus,
84
+ MessageRejected, MissingAttribute => e
80
85
  dont_acknowledge message, '', e.to_s
81
86
  end
82
87
 
88
+ def process_deferred
89
+ site.process_deferred
90
+ end
91
+
83
92
  def acknowledged_first_ingoing message
84
93
  # TODO
85
94
  # aggregateds status should only be send for later version of rsmp
@@ -101,7 +110,7 @@ module RSMP
101
110
  end
102
111
 
103
112
  def reconnect_delay
104
- interval = @site_settings["reconnect_interval"]
113
+ interval = @site_settings['intervals']['watchdog']
105
114
  log "Waiting #{interval} seconds before trying to reconnect", level: :info
106
115
  @task.sleep interval
107
116
  end
@@ -116,9 +125,9 @@ module RSMP
116
125
 
117
126
  def send_aggregated_status component
118
127
  message = AggregatedStatus.new({
119
- "aSTS" => RSMP.now_string,
128
+ "aSTS" => clock.to_s,
120
129
  "cId" => component.c_id,
121
- "fP" => nil,
130
+ "fP" => 'NormalControl',
122
131
  "fS" => nil,
123
132
  "se" => component.aggregated_status_bools
124
133
  })
@@ -154,11 +163,21 @@ module RSMP
154
163
  sorted
155
164
  end
156
165
 
166
+ def process_aggregated_status_request message
167
+ log "Received #{message.type}", message: message, level: :log
168
+ component_id = message.attributes["cId"]
169
+ component = @site.find_component component_id
170
+ acknowledge message
171
+ send_aggregated_status component
172
+ end
173
+
157
174
  def process_command_request message
158
- log "Received #{message.type}", message: message, level: :log
175
+ log "Received #{message.type}", message: message, level: :log
176
+ component_id = message.attributes["cId"]
177
+ component = @site.find_component component_id
159
178
  commands = simplify_command_requests message.attributes["arg"]
160
179
  commands.each_pair do |command_code,arg|
161
- @site.handle_command(command_code,arg).merge('cCI'=>command_code)
180
+ component.handle_command command_code,arg
162
181
  end
163
182
 
164
183
  rvs = message.attributes["arg"].map do |item|
@@ -167,26 +186,27 @@ module RSMP
167
186
  item
168
187
  end
169
188
  response = CommandResponse.new({
170
- "cId"=>message.attributes["cId"],
171
- "cTS"=>RSMP.now_string,
189
+ "cId"=>component_id,
190
+ "cTS"=>clock.to_s,
172
191
  "rvs"=>rvs
173
192
  })
174
193
  acknowledge message
175
194
  send_message response
176
195
  end
177
196
 
178
- def process_status_request message
197
+ def process_status_request message, options={}
179
198
  component_id = message.attributes["cId"]
180
199
  component = @site.find_component component_id
181
200
  log "Received #{message.type}", message: message, level: :log
182
201
  sS = message.attributes["sS"].map do |arg|
183
- value, quality = @site.get_status arg['sCI'], arg['n']
184
- { "s" => value, "q" => quality }.merge arg
202
+ value, quality = component.get_status arg['sCI'], arg['n']
203
+ { "s" => value.to_s, "q" => quality.to_s }.merge arg
185
204
  end
186
205
  response = StatusResponse.new({
187
206
  "cId"=>component_id,
188
- "sTs"=>RSMP.now_string,
189
- "sS"=>sS
207
+ "sTs"=>clock.to_s,
208
+ "sS"=>sS,
209
+ "mId" => options[:m_id]
190
210
  })
191
211
  acknowledge message
192
212
  send_message response
@@ -211,10 +231,11 @@ module RSMP
211
231
  update_list[component] ||= {}
212
232
 
213
233
  subs = @status_subscriptions[component]
234
+ now = Time.now # internal timestamp
214
235
 
215
236
  message.attributes["sS"].each do |arg|
216
237
  sCI = arg["sCI"]
217
- subcription = {interval: arg["uRt"].to_i, last_sent_at: nil}
238
+ subcription = {interval: arg["uRt"].to_i, last_sent_at: now}
218
239
  subs[sCI] ||= {}
219
240
  subs[sCI][arg["n"]] = subcription
220
241
 
@@ -248,18 +269,40 @@ module RSMP
248
269
  status_update_timer now if ready?
249
270
  end
250
271
 
272
+ def fetch_last_sent_status component, code, name
273
+ if @last_status_sent && @last_status_sent[component] && @last_status_sent[component][code]
274
+ @last_status_sent[component][code][name]
275
+ else
276
+ nil
277
+ end
278
+ end
279
+
280
+ def store_last_sent_status component, code, name, value
281
+ @last_status_sent ||= {}
282
+ @last_status_sent[component] ||= {}
283
+ @last_status_sent[component][code] ||= {}
284
+ @last_status_sent[component][code][name] = value
285
+ end
286
+
251
287
  def status_update_timer now
252
288
  update_list = {}
253
289
  # go through subscriptons and build a similarly organized list,
254
290
  # that only contains what should be send
255
291
 
256
292
  @status_subscriptions.each_pair do |component,by_code|
293
+ component_object = @site.find_component component
257
294
  by_code.each_pair do |code,by_name|
258
295
  by_name.each_pair do |name,subscription|
296
+ current = nil
259
297
  if subscription[:interval] == 0
260
298
  # send as soon as the data changes
261
- if rand(100) >= 90
299
+ if component_object
300
+ current, age = *(component_object.get_status code, name)
301
+ end
302
+ last_sent = fetch_last_sent_status component, code, name
303
+ if current != last_sent
262
304
  should_send = true
305
+ store_last_sent_status component, code, name, current
263
306
  end
264
307
  else
265
308
  # send at regular intervals
@@ -270,24 +313,27 @@ module RSMP
270
313
  if should_send
271
314
  subscription[:last_sent_at] = now
272
315
  update_list[component] ||= {}
273
- update_list[component][code] ||= []
274
- update_list[component][code] << name
316
+ update_list[component][code] ||= {}
317
+ update_list[component][code][name] = current
275
318
  end
276
319
  end
277
320
  end
278
321
  end
279
322
  send_status_updates update_list
280
- rescue StandardError => e
281
- log ["Status update exception: #{e}",e.backtrace].flatten.join("\n"), level: :error
282
323
  end
283
324
 
284
325
  def send_status_updates update_list
285
- now = RSMP.now_string
286
- update_list.each_pair do |component,by_code|
326
+ now = clock.to_s
327
+ update_list.each_pair do |component_id,by_code|
328
+ component = @site.find_component component_id
287
329
  sS = []
288
330
  by_code.each_pair do |code,names|
289
- names.map do |status_name|
290
- value,quality = @site.get_status code, status_name
331
+ names.map do |status_name,value|
332
+ if value
333
+ quality = 'recent'
334
+ else
335
+ value,quality = component.get_status code, status_name
336
+ end
291
337
  sS << { "sCI" => code,
292
338
  "n" => status_name,
293
339
  "s" => value.to_s,
@@ -295,7 +341,7 @@ module RSMP
295
341
  end
296
342
  end
297
343
  update = StatusUpdate.new({
298
- "cId"=>component,
344
+ "cId"=>component_id,
299
345
  "sTs"=>now,
300
346
  "sS"=>sS
301
347
  })
@@ -305,7 +351,7 @@ module RSMP
305
351
 
306
352
  def send_alarm
307
353
  message = Alarm.new({
308
- "aSTS"=>RSMP.now_string,
354
+ "aSTS"=>clock.to_s,
309
355
  "fP"=>nil,
310
356
  "fS"=>nil,
311
357
  "se"=>@site.aggregated_status_bools