rsmp 0.52.0 → 0.53.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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +3 -0
  3. data/Gemfile.lock +1 -1
  4. data/documentation/configuration.md +53 -2
  5. data/lib/rsmp/component/component_base.rb +10 -1
  6. data/lib/rsmp/component/component_proxy.rb +1 -1
  7. data/lib/rsmp/component/components.rb +24 -2
  8. data/lib/rsmp/node/site/site.rb +21 -7
  9. data/lib/rsmp/node/site/status.rb +39 -0
  10. data/lib/rsmp/node/supervisor/modules/connection.rb +20 -4
  11. data/lib/rsmp/node/supervisor/modules/sites.rb +2 -0
  12. data/lib/rsmp/options/schemas/required_settings.json +20 -12
  13. data/lib/rsmp/options/schemas/supervisor_site.json +1 -1
  14. data/lib/rsmp/options/site_options.rb +1 -0
  15. data/lib/rsmp/proxy/modules/acknowledgements.rb +2 -0
  16. data/lib/rsmp/proxy/modules/receive.rb +32 -2
  17. data/lib/rsmp/proxy/modules/send.rb +23 -3
  18. data/lib/rsmp/proxy/modules/version_validation.rb +76 -0
  19. data/lib/rsmp/proxy/modules/versions.rb +21 -22
  20. data/lib/rsmp/proxy/proxy.rb +21 -3
  21. data/lib/rsmp/proxy/site/modules/aggregated_status.rb +22 -2
  22. data/lib/rsmp/proxy/site/modules/commands.rb +1 -1
  23. data/lib/rsmp/proxy/site/site_proxy.rb +22 -13
  24. data/lib/rsmp/proxy/site/sxl_selection.rb +13 -22
  25. data/lib/rsmp/proxy/supervisor/modules/aggregated_status.rb +56 -1
  26. data/lib/rsmp/proxy/supervisor/modules/alarms.rb +11 -4
  27. data/lib/rsmp/proxy/supervisor/modules/commands.rb +4 -3
  28. data/lib/rsmp/proxy/supervisor/modules/message_buffer.rb +6 -1
  29. data/lib/rsmp/proxy/supervisor/modules/status.rb +46 -1
  30. data/lib/rsmp/proxy/supervisor/modules/status_updates.rb +17 -1
  31. data/lib/rsmp/proxy/supervisor/supervisor_proxy.rb +12 -16
  32. data/lib/rsmp/schema/validation.rb +2 -1
  33. data/lib/rsmp/version.rb +1 -1
  34. data/schemas/core/3.1.2/definitions.json +5 -0
  35. data/schemas/core/3.1.2/status_subscribe.json +1 -1
  36. data/schemas/core/3.1.3/definitions.json +5 -0
  37. data/schemas/core/3.1.3/status_subscribe.json +1 -1
  38. data/schemas/core/3.1.4/definitions.json +5 -0
  39. data/schemas/core/3.1.4/status_subscribe.json +1 -1
  40. data/schemas/core/3.1.5/definitions.json +5 -0
  41. data/schemas/core/3.1.5/status_subscribe.json +1 -1
  42. data/schemas/core/3.2.0/definitions.json +5 -0
  43. data/schemas/core/3.2.0/status_subscribe.json +1 -1
  44. data/schemas/core/3.2.1/definitions.json +5 -0
  45. data/schemas/core/3.2.1/status_subscribe.json +1 -1
  46. data/schemas/core/3.2.2/definitions.json +5 -0
  47. data/schemas/core/3.2.2/status_subscribe.json +1 -1
  48. data/schemas/core/3.3.0/aggregated_status.json +17 -11
  49. data/schemas/core/3.3.0/aggregated_status_request.json +4 -4
  50. data/schemas/core/3.3.0/component_list.json +1 -1
  51. data/schemas/core/3.3.0/definitions.json +23 -2
  52. data/schemas/core/3.3.0/status_subscribe.json +14 -4
  53. data/schemas/core/3.3.0/version.json +46 -26
  54. data/schemas/tlc/1.0.10/defs/definitions.json +5 -0
  55. data/schemas/tlc/1.0.13/defs/definitions.json +5 -0
  56. data/schemas/tlc/1.0.14/defs/definitions.json +5 -0
  57. data/schemas/tlc/1.0.15/defs/definitions.json +5 -0
  58. data/schemas/tlc/1.0.7/defs/definitions.json +5 -0
  59. data/schemas/tlc/1.0.8/defs/definitions.json +5 -0
  60. data/schemas/tlc/1.0.9/defs/definitions.json +5 -0
  61. data/schemas/tlc/1.1.0/defs/definitions.json +5 -0
  62. data/schemas/tlc/1.2.0/defs/definitions.json +5 -0
  63. data/schemas/tlc/1.2.1/defs/definitions.json +5 -0
  64. data/schemas/tlc/1.3.0/defs/definitions.json +7 -0
  65. metadata +3 -1
@@ -1,9 +1,13 @@
1
+ require_relative 'version_validation'
2
+
1
3
  module RSMP
2
4
  class Proxy
3
5
  module Modules
4
6
  # Version negotiation and handling
5
7
  # Manages RSMP version handshake between sites and supervisors
6
8
  module Versions
9
+ include VersionValidation
10
+
7
11
  def core_versions
8
12
  version = @site_settings['core_version']
9
13
  if version == 'latest'
@@ -25,8 +29,10 @@ module RSMP
25
29
  versions.uniq
26
30
  end
27
31
 
28
- def core_3_3?
29
- core_version && version_meets_requirement?(core_version, '>=3.3.0')
32
+ def core_3_3_0_or_newer?
33
+ return false unless core_version
34
+
35
+ version_meets_requirement?(core_version, '>=3.3.0')
30
36
  end
31
37
 
32
38
  def configured_sxls
@@ -48,7 +54,8 @@ module RSMP
48
54
  end
49
55
 
50
56
  def check_core_version(message)
51
- versions = core_versions
57
+ validate_version_array!(message)
58
+ versions = @offered_core_versions || core_versions
52
59
  candidates = message.versions.filter_map do |wire_version|
53
60
  normalized = RSMP::Schema.normalize_core_version(wire_version)
54
61
  [wire_version, normalized] if normalized && versions.include?(normalized)
@@ -60,7 +67,6 @@ module RSMP
60
67
  else
61
68
  reason = "RSMP versions [#{message.versions.join(', ')}] requested, " \
62
69
  "but only [#{versions.join(', ')}] supported."
63
- dont_acknowledge message, 'Version message rejected', reason, force: true
64
70
  raise HandshakeError, reason
65
71
  end
66
72
  end
@@ -80,13 +86,13 @@ module RSMP
80
86
  end
81
87
 
82
88
  def send_version_response(site_id, core_versions)
83
- if core_3_3?
89
+ if core_3_3_0_or_newer?
84
90
  send_generated_message Version.new({
85
91
  'step' => 'Response',
86
92
  'RSMP' => [{ 'vers' => core_version }],
87
93
  'supervisorId' => site_id,
88
94
  'SXLS' => version_response_sxls,
89
- 'receiveAlarms' => @site_settings['receive_alarms'] != false
95
+ 'useAlarms' => @site_settings['use_alarms'] != false
90
96
  }), validate: false
91
97
  else
92
98
  send_legacy_version_response(site_id, core_versions)
@@ -106,7 +112,14 @@ module RSMP
106
112
 
107
113
  def send_version_message(site_id, core_versions, step:)
108
114
  attributes = version_message_attributes(site_id, core_versions)
109
- attributes.merge!(version_request_attributes) if step == 'Request'
115
+ if step == 'Request'
116
+ @offered_core_versions = normalized_core_versions(core_versions).filter_map do |version|
117
+ RSMP::Schema.normalize_core_version(version)
118
+ end
119
+ legacy_sxl = attributes['SXL']
120
+ attributes.merge!(version_request_attributes)
121
+ attributes['SXL'] = legacy_sxl unless @offered_core_versions.include?('3.3.0')
122
+ end
110
123
  send_generated_message Version.new(attributes), validate: false
111
124
  end
112
125
 
@@ -151,6 +164,7 @@ module RSMP
151
164
  def version_request_attributes
152
165
  {
153
166
  'step' => 'Request',
167
+ 'SXL' => primary_configured_sxl ? sxl_request_items.first['version'] : '',
154
168
  'SXLS' => sxl_request_items
155
169
  }
156
170
  end
@@ -159,21 +173,6 @@ module RSMP
159
173
  accepted_sxls + rejected_sxls
160
174
  end
161
175
 
162
- def validate_sxl_response!(sxls)
163
- invalid = sxls.find { |accepted| !valid_sxl_response?(accepted) }
164
- return unless invalid
165
-
166
- raise HandshakeError,
167
- "Invalid SXL version #{invalid['name']} #{invalid['version']}; " \
168
- 'Core 3.3 requires an exact MAJOR.MINOR.PATCH match'
169
- end
170
-
171
- def valid_sxl_response?(accepted)
172
- requested = sxl_request_items.find { |item| item['name'] == accepted['name'] }
173
- RSMP::Schema.strict_version?(accepted['version']) && requested &&
174
- requested['version'] == accepted['version']
175
- end
176
-
177
176
  def version_acknowledged; end
178
177
 
179
178
  def component_list_acknowledged; end
@@ -80,6 +80,10 @@ module RSMP
80
80
  @ingoing_acknowledged = {}
81
81
  @outgoing_acknowledged = {}
82
82
  @latest_watchdog_send_at = nil
83
+ @last_site_status = nil
84
+ @last_component_list = nil
85
+ @initial_aggregated_status_id = nil
86
+ @site_aggregated_status = nil
83
87
  @component_list_received = false
84
88
  @outgoing_watchdog_acknowledged = false
85
89
  @sxl_interfaces = {}
@@ -104,7 +108,7 @@ module RSMP
104
108
  @sxls = []
105
109
  @accepted_sxls = []
106
110
  @rejected_sxls = []
107
- @receive_alarms = true
111
+ @use_alarms = true
108
112
  @site_settings = nil # can't pick until we know the site id
109
113
  collect = options[:collect]
110
114
  return unless collect
@@ -135,8 +139,22 @@ module RSMP
135
139
  schemas
136
140
  end
137
141
 
138
- def receive_alarms?
139
- @receive_alarms != false
142
+ def application_message?(message)
143
+ !%w[Version Watchdog MessageAck MessageNotAck ComponentList AggregatedStatus].include?(message.type)
144
+ end
145
+
146
+ def sxl_message_allowed?(message)
147
+ return true unless core_3_3_0_or_newer?
148
+ return false if message.is_a?(Alarm) && !use_alarms?
149
+ return true if RSMP::Schema.core_message_type?(message.attributes)
150
+
151
+ !RSMP::Schema.resolve_sxl(message.attributes, schemas: schemas).nil?
152
+ rescue RSMP::Schema::UnknownMessageCodeError
153
+ false
154
+ end
155
+
156
+ def use_alarms?
157
+ !core_3_3_0_or_newer? || @use_alarms != false
140
158
  end
141
159
 
142
160
  def primary_sxl
@@ -4,7 +4,7 @@ module RSMP
4
4
  # Handles aggregated status requests and responses
5
5
  module AggregatedStatus
6
6
  # Build and send an AggregatedStatusRequest. Returns Result<AggregatedStatusRequest>.
7
- def request_aggregated_status(component, m_id: nil, validate: true)
7
+ def request_aggregated_status(component = nil, m_id: nil, validate: true)
8
8
  readiness = validate_ready 'request aggregated status'
9
9
  return readiness if readiness.failure?
10
10
 
@@ -13,6 +13,7 @@ module RSMP
13
13
  'cId' => component,
14
14
  'mId' => m_id
15
15
  })
16
+ message.attributes.delete('cId') if core_3_3_0_or_newer?
16
17
  apply_nts_message_attributes message
17
18
  send_message(message, validate: validate).map(&:message)
18
19
  end
@@ -22,7 +23,7 @@ module RSMP
22
23
  end
23
24
 
24
25
  # Build, send an AggregatedStatusRequest and collect the response. Returns Result<Exchange>.
25
- def request_aggregated_status_and_collect(component, within:, m_id: nil, validate: true)
26
+ def request_aggregated_status_and_collect(component = nil, within:, m_id: nil, validate: true)
26
27
  readiness = validate_ready 'request aggregated status'
27
28
  return readiness if readiness.failure?
28
29
 
@@ -31,6 +32,7 @@ module RSMP
31
32
  'cId' => component,
32
33
  'mId' => m_id
33
34
  })
35
+ message.attributes.delete('cId') if core_3_3_0_or_newer?
34
36
  apply_nts_message_attributes message
35
37
  collector = AggregatedStatusCollector.new(self, timeout: within, m_id: m_id, num: 1)
36
38
  send_message_and_collect(message, collector, validate: validate)
@@ -48,6 +50,8 @@ module RSMP
48
50
  end
49
51
 
50
52
  def process_aggregated_status(message)
53
+ return process_site_aggregated_status(message) if core_3_3_0_or_newer?
54
+
51
55
  status_elements = message.attribute('se')
52
56
  validate_aggregated_status(message, status_elements)
53
57
  c_id = message.attributes['cId']
@@ -64,6 +68,22 @@ module RSMP
64
68
  acknowledge message
65
69
  end
66
70
 
71
+ def process_site_aggregated_status(message)
72
+ @site_aggregated_status = message.attribute('se').dup.freeze
73
+ acknowledge message
74
+ @supervisor.site_aggregated_status_changed(self)
75
+ complete_initial_status(message)
76
+ end
77
+
78
+ attr_reader :site_aggregated_status
79
+
80
+ def complete_initial_status(message)
81
+ return unless core_3_3_0_or_newer? && !ready? && @component_list_received && @outgoing_watchdog_acknowledged
82
+ return unless message.is_a?(RSMP::AggregatedStatus)
83
+
84
+ handshake_complete
85
+ end
86
+
67
87
  def aggregated_status_changed(component, _options = {})
68
88
  @supervisor.aggregated_status_changed self, component
69
89
  end
@@ -66,7 +66,7 @@ module RSMP
66
66
  end
67
67
 
68
68
  def process_command_response(message)
69
- return reject_multiple_command_codes(message) if core_3_3? && multiple_command_codes?(message)
69
+ return reject_multiple_command_codes(message) if core_3_3_0_or_newer? && multiple_command_codes?(message)
70
70
 
71
71
  log "Received #{message.type}", message: message, level: :log
72
72
  acknowledge message
@@ -86,8 +86,9 @@ module RSMP
86
86
  log "Received Version message for site #{@site_id}", message: message, level: :log
87
87
  start_timer
88
88
  acknowledge message
89
- response_id = core_3_3? ? (@supervisor.site_id || @site_id) : @site_id
89
+ response_id = core_3_3_0_or_newer? ? (@supervisor.site_id || @site_id) : @site_id
90
90
  send_version_response response_id, core_versions
91
+ @use_alarms = @site_settings['use_alarms'] != false if core_3_3_0_or_newer?
91
92
  @version_determined = true
92
93
  end
93
94
 
@@ -101,9 +102,9 @@ module RSMP
101
102
  def acknowledged_first_outgoing(message)
102
103
  case message.type
103
104
  when 'Watchdog'
104
- if core_3_3?
105
+ if core_3_3_0_or_newer?
105
106
  @outgoing_watchdog_acknowledged = true
106
- handshake_complete if @component_list_received
107
+
107
108
  else
108
109
  handshake_complete
109
110
  end
@@ -130,30 +131,35 @@ module RSMP
130
131
  def process_component_list(message)
131
132
  log "Received #{message.type}", message: message, level: :log
132
133
  rebuild_components_from_list message.attributes['components']
133
- build_sxl_interfaces
134
134
  acknowledge message
135
135
  @component_list_received = true
136
- handshake_complete if @outgoing_watchdog_acknowledged
137
136
  end
138
137
 
139
138
  def rebuild_components_from_list(items)
140
139
  main_id = @site_settings.dig('components', 'main')&.keys&.first
140
+ ids = items.map { |item| item['id'] }
141
+ raise MessageRejected, 'Duplicate component IDs' unless ids.uniq == ids
142
+
143
+ previous = @components
141
144
  @components = {}
142
145
  @main = nil
143
146
  items.each do |item|
144
147
  grouped = item['id'] == main_id
145
- component = ComponentProxy.new(
146
- id: item['id'],
147
- node: self,
148
- type: item['type'],
149
- name: item['name'],
150
- grouped: grouped
151
- )
148
+ component = listed_component(item, previous[item['id']], grouped)
152
149
  @components[component.c_id] = component
153
150
  @main = component if grouped
154
151
  end
155
152
  end
156
153
 
154
+ def listed_component(item, previous, grouped)
155
+ if previous && previous.component_type == item['type']
156
+ previous.update_metadata(name: item['name'] || item['id'])
157
+ return previous
158
+ end
159
+
160
+ ComponentProxy.new(id: item['id'], node: self, type: item['type'], name: item['name'], grouped: grouped)
161
+ end
162
+
157
163
  def site_ids_changed
158
164
  @supervisor.site_ids_changed
159
165
  end
@@ -165,8 +171,9 @@ module RSMP
165
171
  def process_version(message)
166
172
  return extraneous_version message if @version_determined
167
173
 
168
- check_site_ids message
169
174
  check_core_version message
175
+ validate_version! message, step: 'Request'
176
+ check_site_ids message
170
177
  check_sxl_version message
171
178
  version_accepted message
172
179
  end
@@ -174,6 +181,8 @@ module RSMP
174
181
  def check_site_ids(message)
175
182
  # RSMP support multiple site ids. we don't support this yet. instead we use the first id only
176
183
  site_id = message.attribute('siteId').map { |item| item['sId'] }.first
184
+ raise HandshakeError, 'Unexpected site identity' if core_3_3_0_or_newer? && @site_id && @site_id != site_id
185
+
177
186
  @site_id ||= site_id
178
187
  @supervisor.check_site_id site_id
179
188
  site_ids_changed
@@ -2,7 +2,7 @@ module RSMP
2
2
  # Selects the SXL versions accepted by a supervisor-side site proxy.
3
3
  module SiteSxlSelection
4
4
  def check_sxl_version(message)
5
- if core_3_3?
5
+ if core_3_3_0_or_newer?
6
6
  select_sxls message
7
7
  else
8
8
  select_legacy_sxl message
@@ -26,34 +26,25 @@ module RSMP
26
26
  end
27
27
 
28
28
  def select_sxls(message)
29
- selected_sxls = message.sxls.map { |requested| select_sxl(requested) }
30
- @accepted_sxls, @rejected_sxls = selected_sxls.partition { |item| item['rejected'].nil? }
29
+ selected = message.sxls.map { |requested| select_sxl(requested) }
30
+ offered = message.sxls.map { |item| item['name'] }
31
+ configured_sxls.each do |item|
32
+ selected << { 'name' => item['name'], 'status' => 'expected' } unless offered.include?(item['name'])
33
+ end
34
+ @accepted_sxls, @rejected_sxls = selected.partition { |item| item['status'] == 'ok' }
31
35
  end
32
36
 
33
37
  def select_sxl(requested)
34
38
  configured = configured_sxls.find { |item| item['name'] == requested['name'] }
35
- return rejected_sxl(requested, 1, 'SXL not supported') unless configured
36
-
37
- unless RSMP::Schema.strict_version?(requested['version'])
38
- return rejected_sxl(requested, 2, 'Core 3.3 requires SXL versions to use MAJOR.MINOR.PATCH')
39
- end
39
+ return { 'name' => requested['name'], 'status' => 'unsupported' } unless configured
40
40
 
41
- configured_version = RSMP::Schema.sanitize_version(configured['version'].to_s)
42
- if configured_version == requested['version']
43
- RSMP::Schema.find_schema! requested['name'], requested['version']
44
- requested.slice('name', 'version', 'prefix')
41
+ version = RSMP::Schema.sanitize_version(configured['version'].to_s)
42
+ if version == requested['version']
43
+ RSMP::Schema.find_schema! requested['name'], version
44
+ { 'name' => requested['name'], 'status' => 'ok', 'version' => version }
45
45
  else
46
- rejected_sxl(requested, 2, "Supervisor only supports #{configured['version']}")
46
+ { 'name' => requested['name'], 'status' => 'mismatch', 'supported' => [version] }
47
47
  end
48
48
  end
49
-
50
- def rejected_sxl(requested, code, reason)
51
- {
52
- 'name' => requested['name'],
53
- 'version' => requested['version'],
54
- 'rejected' => code,
55
- 'reason' => reason
56
- }.compact
57
- end
58
49
  end
59
50
  end
@@ -3,14 +3,44 @@ module RSMP
3
3
  module Modules
4
4
  # Handles aggregated status messages
5
5
  module AggregatedStatus
6
+ def send_component_list
7
+ components = @site.component_list
8
+ result = send_generated_message ComponentList.new('components' => components)
9
+ @last_component_list = JSON.parse(JSON.generate(components)) if result.success?
10
+ result
11
+ end
12
+
13
+ def synchronize_site_state
14
+ send_component_list if @site.component_list != @last_component_list
15
+ send_changed_site_status
16
+ end
17
+
18
+ def component_list_acknowledged
19
+ return if ready? || @initial_aggregated_status_id
20
+
21
+ @initial_aggregated_status_id = Message.make_m_id
22
+ send_aggregated_status(nil, m_id: @initial_aggregated_status_id)
23
+ end
24
+
25
+ def initial_status_acknowledged(original)
26
+ return unless core_3_3_0_or_newer? && original.m_id == @initial_aggregated_status_id
27
+
28
+ @initial_aggregated_status_id = nil
29
+ handshake_complete unless ready?
30
+ end
31
+
6
32
  def send_all_aggregated_status
33
+ return send_site_aggregated_status if core_3_3_0_or_newer?
34
+
7
35
  @site.components.each_pair do |_c_id, component|
8
36
  send_aggregated_status component if component.grouped
9
37
  end
10
38
  end
11
39
 
12
40
  # Send aggregated status for a component
13
- def send_aggregated_status(component, m_id: nil)
41
+ def send_aggregated_status(component = nil, m_id: nil)
42
+ return send_site_aggregated_status(m_id: m_id) if core_3_3_0_or_newer?
43
+
14
44
  m_id ||= RSMP::Message.make_m_id
15
45
 
16
46
  se = if core_version && Proxy.version_meets_requirement?(core_version, '<=3.1.2')
@@ -32,8 +62,33 @@ module RSMP
32
62
  send_generated_message message
33
63
  end
34
64
 
65
+ def send_changed_site_status
66
+ bits = @site.site_aggregated_status
67
+ return if bits == @last_site_status
68
+ return if connected? && !ready?
69
+
70
+ result = send_site_aggregated_status
71
+ @last_site_status = bits if result.success?
72
+ result
73
+ end
74
+
75
+ def send_site_aggregated_status(m_id: nil)
76
+ message = RSMP::AggregatedStatus.new(
77
+ 'mId' => m_id, 'aSTS' => clock.to_s, 'se' => @site.site_aggregated_status
78
+ )
79
+ apply_nts_message_attributes message
80
+ result = send_generated_message message
81
+ @last_site_status = message.attributes['se'].dup if result.success?
82
+ result
83
+ end
84
+
35
85
  def process_aggregated_status_request(message)
36
86
  log "Received #{message.type}", message: message, level: :log
87
+ if core_3_3_0_or_newer?
88
+ acknowledge message
89
+ return send_site_aggregated_status
90
+ end
91
+
37
92
  component_id = message.attributes['cId']
38
93
  component = @site.find_component component_id
39
94
  acknowledge message
@@ -6,14 +6,21 @@ module RSMP
6
6
  def send_active_alarms
7
7
  @site.components.each_pair do |_c_id, component|
8
8
  component.alarms.each_pair do |_alarm_code, alarm_state|
9
- if alarm_state.active
10
- alarm = AlarmIssue.new(alarm_state.to_hash.merge('aSp' => 'Issue'))
11
- send_generated_message alarm
12
- end
9
+ next unless alarm_state.active || core_3_3_0_or_newer?
10
+
11
+ alarm = AlarmIssue.new(alarm_state.to_hash.merge('aSp' => 'Issue'))
12
+ next if core_3_3_0_or_newer? && !sxl_message_allowed?(alarm)
13
+
14
+ send_generated_message alarm
15
+ @initial_alarm_events << alarm_event_key(alarm) if core_3_3_0_or_newer? && @initial_alarm_events
13
16
  end
14
17
  end
15
18
  end
16
19
 
20
+ def alarm_event_key(message)
21
+ message.attributes.except('mId', 'ntsOId', 'xNId')
22
+ end
23
+
17
24
  def process_alarm(message)
18
25
  case message
19
26
  when AlarmAcknowledge
@@ -49,7 +49,7 @@ module RSMP
49
49
  end
50
50
 
51
51
  def check_required_command_arguments(message)
52
- return unless core_3_3?
52
+ return unless core_3_3_0_or_newer?
53
53
 
54
54
  command_codes(message).each do |command_code|
55
55
  provided = message.attributes['arg'].select { |item| item['cCI'] == command_code }.map { |item| item['n'] }
@@ -93,11 +93,12 @@ module RSMP
93
93
  end
94
94
 
95
95
  def process_command_request(message)
96
- return reject_multiple_command_codes(message) if core_3_3? && multiple_command_codes?(message)
96
+ return reject_multiple_command_codes(message) if core_3_3_0_or_newer? && multiple_command_codes?(message)
97
97
 
98
98
  check_required_command_arguments message
99
99
  component_id = message.attributes['cId']
100
100
  rvs = build_command_rvs(message.attributes['arg'])
101
+ acknowledge message if core_3_3_0_or_newer?
101
102
  execute_commands(message, component_id, rvs)
102
103
 
103
104
  response = CommandResponse.new({
@@ -106,7 +107,7 @@ module RSMP
106
107
  'rvs' => rvs
107
108
  })
108
109
  apply_nts_message_attributes response
109
- acknowledge message
110
+ acknowledge message unless core_3_3_0_or_newer?
110
111
  send_generated_message response
111
112
  end
112
113
  end
@@ -76,8 +76,9 @@ module RSMP
76
76
 
77
77
  def prepare_message_for_buffer(message, core_version: @core_version, for_send: false)
78
78
  return unless site_originated_buffer_candidate? message
79
+ return unless sxl_message_allowed?(message)
79
80
  return false if message.is_a?(RSMP::StatusUpdate) && status_buffer_selectors == false
80
- return false if message.is_a?(RSMP::Alarm) && !receive_alarms?
81
+ return false if message.is_a?(RSMP::Alarm) && !use_alarms?
81
82
 
82
83
  if message.is_a? RSMP::AggregatedStatus
83
84
  prepare_aggregated_status_for_buffer message, core_version: core_version, for_send: for_send
@@ -121,6 +122,10 @@ module RSMP
121
122
  end
122
123
 
123
124
  def flush_buffered_message?(message, queued, index)
125
+ if core_3_3_0_or_newer? && message.is_a?(Alarm) && @initial_alarm_events&.include?(alarm_event_key(message))
126
+ return true
127
+ end
128
+
124
129
  prepared = prepare_message_for_buffer message, core_version: @core_version, for_send: true
125
130
  return true unless prepared
126
131
 
@@ -60,7 +60,8 @@ module RSMP
60
60
  def add_status_subscription(component_id, subs, update_list, arg, now)
61
61
  sci = arg['sCI']
62
62
  name = arg['n']
63
- subcription = { interval: arg['uRt'].to_i, last_sent_at: now }
63
+ subcription = { interval: arg['uRt'].to_f, last_sent_at: now }
64
+ subcription[:on_change] = arg['sOc'] if core_3_3_0_or_newer?
64
65
  subs[sci] ||= {}
65
66
  subs[sci][name] = subcription
66
67
  update_list[component_id][sci] ||= []
@@ -70,6 +71,8 @@ module RSMP
70
71
  def process_status_subcribe(message)
71
72
  log "Received #{message.type}", message: message, level: :log
72
73
 
74
+ return process_core_3_3_subscription(message) if core_3_3_0_or_newer?
75
+
73
76
  update_list = {}
74
77
  component_id = message.attributes['cId']
75
78
  @status_subscriptions[component_id] ||= {}
@@ -84,6 +87,48 @@ module RSMP
84
87
  send_status_updates update_list
85
88
  end
86
89
 
90
+ def process_core_3_3_subscription(message)
91
+ validate_core_3_3_subscription!(message)
92
+ id = message.attributes['cId']
93
+ args = message.attributes['sS']
94
+ component = @site.find_component(id, build: false)
95
+ values = component ? fetch_status_values(component, args) : build_undefined_statuses(args)
96
+ subs = (@status_subscriptions[id] ||= {})
97
+ update_core_3_3_subscriptions(id, args, values, subs)
98
+ @status_subscriptions.delete(id) if subs.empty?
99
+ acknowledge message
100
+ update = core_3_3_subscription_update(id, values)
101
+ send_generated_message update
102
+ store_last_sent_status update
103
+ end
104
+
105
+ def core_3_3_subscription_update(id, values)
106
+ values = values.map { |value| value.except('uRt', 'sOc') }
107
+ StatusUpdate.new('cId' => id, 'sTs' => clock.to_s, 'sS' => values)
108
+ end
109
+
110
+ def update_core_3_3_subscriptions(id, args, values, subs)
111
+ updates = { id => {} }
112
+ values.each_with_index do |value, index|
113
+ arg = args[index]
114
+ if %w[unknown undefined].include?(value['q'])
115
+ remove_status_subscription(subs, arg)
116
+ else
117
+ add_status_subscription(id, subs, updates, arg, Time.now)
118
+ end
119
+ end
120
+ end
121
+
122
+ def validate_core_3_3_subscription!(message)
123
+ invalid = message.attributes['sS'].any? do |arg|
124
+ interval = arg['uRt'].to_f
125
+ interval.negative? || (interval.zero? && !arg['sOc'])
126
+ end
127
+ return unless invalid
128
+
129
+ raise MessageRejected, 'Subscription interval must be nonnegative and enable interval or sendOnChange'
130
+ end
131
+
87
132
  def get_status_subscribe_interval(component_id, sci, name)
88
133
  @status_subscriptions.dig component_id, sci, name
89
134
  end
@@ -22,7 +22,8 @@ module RSMP
22
22
  end
23
23
 
24
24
  def check_on_change_update(subscription, component, code, name)
25
- return [nil, false] unless subscription[:interval].zero?
25
+ enabled = core_3_3_0_or_newer? ? subscription[:on_change] : subscription[:interval].zero?
26
+ return [nil, false] unless enabled
26
27
 
27
28
  current = current_status_value(component, code, name)
28
29
  last_sent = fetch_last_sent_status component.c_id, code, name
@@ -34,6 +35,10 @@ module RSMP
34
35
 
35
36
  value, quality = *(component.get_status code, name)
36
37
  rsmpify_value(value, quality)
38
+ rescue UnknownStatus
39
+ raise unless core_3_3_0_or_newer?
40
+
41
+ nil
37
42
  end
38
43
 
39
44
  def encode_status_value(code, name, value)
@@ -59,6 +64,8 @@ module RSMP
59
64
  end
60
65
 
61
66
  def status_update_timer(now)
67
+ return if core_3_3_0_or_newer? && connected? && !ready?
68
+
62
69
  send_status_updates status_updates_due(now)
63
70
  end
64
71
 
@@ -71,8 +78,15 @@ module RSMP
71
78
  end
72
79
 
73
80
  def collect_component_status_updates(update_list, component_id, by_code, now)
81
+ if core_3_3_0_or_newer? && !@site.components.key?(component_id)
82
+ @status_subscriptions.delete(component_id)
83
+ return
84
+ end
74
85
  component = @site.find_component component_id
75
86
  by_code.each_pair do |code, by_name|
87
+ probe = StatusUpdate.new('sS' => [{ 'sCI' => code }])
88
+ next if core_3_3_0_or_newer? && !sxl_message_allowed?(probe)
89
+
76
90
  by_name.each_pair do |name, subscription|
77
91
  current, should_send = check_status_subscription(subscription, component, code, name, now)
78
92
  next unless should_send
@@ -90,6 +104,8 @@ module RSMP
90
104
  end
91
105
 
92
106
  def precomputed_status_value(subscription, current)
107
+ return if core_3_3_0_or_newer? # Fetch value and quality together; do not replace quality with recent.
108
+
93
109
  PrecomputedStatusValue.new(current) if subscription[:interval].zero?
94
110
  end
95
111