rsmp 0.37.0 → 0.39.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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/devcontainer.json +22 -0
  3. data/.github/workflows/rubocop.yaml +17 -0
  4. data/.gitignore +5 -6
  5. data/.rubocop.yml +69 -0
  6. data/.tool-versions +1 -1
  7. data/Gemfile +14 -1
  8. data/Gemfile.lock +64 -29
  9. data/Rakefile +3 -3
  10. data/lib/rsmp/cli.rb +148 -124
  11. data/lib/rsmp/collect/ack_collector.rb +8 -7
  12. data/lib/rsmp/collect/aggregated_status_collector.rb +4 -4
  13. data/lib/rsmp/collect/alarm_collector.rb +31 -23
  14. data/lib/rsmp/collect/alarm_matcher.rb +6 -6
  15. data/lib/rsmp/collect/collector/logging.rb +18 -0
  16. data/lib/rsmp/collect/collector/reporting.rb +44 -0
  17. data/lib/rsmp/collect/collector/status.rb +34 -0
  18. data/lib/rsmp/collect/collector.rb +69 -150
  19. data/lib/rsmp/collect/command_matcher.rb +19 -6
  20. data/lib/rsmp/collect/command_response_collector.rb +7 -7
  21. data/lib/rsmp/collect/distributor.rb +14 -11
  22. data/lib/rsmp/collect/filter.rb +31 -15
  23. data/lib/rsmp/collect/matcher.rb +9 -13
  24. data/lib/rsmp/collect/queue.rb +7 -7
  25. data/lib/rsmp/collect/receiver.rb +11 -15
  26. data/lib/rsmp/collect/state_collector.rb +116 -77
  27. data/lib/rsmp/collect/status_collector.rb +6 -6
  28. data/lib/rsmp/collect/status_matcher.rb +15 -4
  29. data/lib/rsmp/{alarm_state.rb → component/alarm_state.rb} +76 -38
  30. data/lib/rsmp/{component.rb → component/component.rb} +15 -15
  31. data/lib/rsmp/component/component_base.rb +88 -0
  32. data/lib/rsmp/component/component_proxy.rb +75 -0
  33. data/lib/rsmp/component/components.rb +62 -0
  34. data/lib/rsmp/convert/export/json_schema.rb +118 -110
  35. data/lib/rsmp/convert/import/yaml.rb +22 -18
  36. data/lib/rsmp/{rsmp.rb → helpers/clock.rb} +8 -11
  37. data/lib/rsmp/{deep_merge.rb → helpers/deep_merge.rb} +3 -1
  38. data/lib/rsmp/helpers/error.rb +72 -0
  39. data/lib/rsmp/helpers/inspect.rb +41 -0
  40. data/lib/rsmp/log/archive.rb +97 -0
  41. data/lib/rsmp/log/colorization.rb +41 -0
  42. data/lib/rsmp/log/filtering.rb +54 -0
  43. data/lib/rsmp/log/logger.rb +207 -0
  44. data/lib/rsmp/{logging.rb → log/logging.rb} +6 -7
  45. data/lib/rsmp/message.rb +185 -148
  46. data/lib/rsmp/{node.rb → node/node.rb} +20 -19
  47. data/lib/rsmp/{protocol.rb → node/protocol.rb} +6 -3
  48. data/lib/rsmp/node/site/site.rb +192 -0
  49. data/lib/rsmp/node/supervisor/modules/configuration.rb +59 -0
  50. data/lib/rsmp/node/supervisor/modules/connection.rb +140 -0
  51. data/lib/rsmp/node/supervisor/modules/sites.rb +64 -0
  52. data/lib/rsmp/node/supervisor/supervisor.rb +69 -0
  53. data/lib/rsmp/{task.rb → node/task.rb} +13 -14
  54. data/lib/rsmp/proxy/modules/acknowledgements.rb +144 -0
  55. data/lib/rsmp/proxy/modules/receive.rb +119 -0
  56. data/lib/rsmp/proxy/modules/send.rb +76 -0
  57. data/lib/rsmp/proxy/modules/state.rb +25 -0
  58. data/lib/rsmp/proxy/modules/tasks.rb +105 -0
  59. data/lib/rsmp/proxy/modules/versions.rb +69 -0
  60. data/lib/rsmp/proxy/modules/watchdogs.rb +66 -0
  61. data/lib/rsmp/proxy/proxy.rb +197 -0
  62. data/lib/rsmp/proxy/site/modules/aggregated_status.rb +52 -0
  63. data/lib/rsmp/proxy/site/modules/alarms.rb +27 -0
  64. data/lib/rsmp/proxy/site/modules/commands.rb +31 -0
  65. data/lib/rsmp/proxy/site/modules/status.rb +110 -0
  66. data/lib/rsmp/proxy/site/site_proxy.rb +204 -0
  67. data/lib/rsmp/proxy/supervisor/modules/aggregated_status.rb +47 -0
  68. data/lib/rsmp/proxy/supervisor/modules/alarms.rb +73 -0
  69. data/lib/rsmp/proxy/supervisor/modules/commands.rb +53 -0
  70. data/lib/rsmp/proxy/supervisor/modules/status.rb +204 -0
  71. data/lib/rsmp/proxy/supervisor/supervisor_proxy.rb +177 -0
  72. data/lib/rsmp/tlc/detector_logic.rb +19 -34
  73. data/lib/rsmp/tlc/input_states.rb +126 -0
  74. data/lib/rsmp/tlc/modules/detector_logics.rb +50 -0
  75. data/lib/rsmp/tlc/modules/display.rb +78 -0
  76. data/lib/rsmp/tlc/modules/helpers.rb +41 -0
  77. data/lib/rsmp/tlc/modules/inputs.rb +173 -0
  78. data/lib/rsmp/tlc/modules/modes.rb +253 -0
  79. data/lib/rsmp/tlc/modules/outputs.rb +30 -0
  80. data/lib/rsmp/tlc/modules/plans.rb +218 -0
  81. data/lib/rsmp/tlc/modules/signal_groups.rb +109 -0
  82. data/lib/rsmp/tlc/modules/startup_sequence.rb +22 -0
  83. data/lib/rsmp/tlc/modules/system.rb +140 -0
  84. data/lib/rsmp/tlc/modules/traffic_data.rb +49 -0
  85. data/lib/rsmp/tlc/signal_group.rb +38 -41
  86. data/lib/rsmp/tlc/signal_plan.rb +14 -11
  87. data/lib/rsmp/tlc/signal_priority.rb +40 -35
  88. data/lib/rsmp/tlc/startup_sequence.rb +59 -0
  89. data/lib/rsmp/tlc/traffic_controller.rb +38 -1010
  90. data/lib/rsmp/tlc/traffic_controller_site.rb +58 -57
  91. data/lib/rsmp/version.rb +1 -1
  92. data/lib/rsmp.rb +82 -48
  93. data/rsmp.gemspec +24 -31
  94. metadata +79 -139
  95. data/lib/rsmp/archive.rb +0 -76
  96. data/lib/rsmp/collect/message_matchers.rb +0 -0
  97. data/lib/rsmp/component_base.rb +0 -87
  98. data/lib/rsmp/component_proxy.rb +0 -57
  99. data/lib/rsmp/components.rb +0 -65
  100. data/lib/rsmp/error.rb +0 -71
  101. data/lib/rsmp/inspect.rb +0 -46
  102. data/lib/rsmp/logger.rb +0 -216
  103. data/lib/rsmp/proxy.rb +0 -693
  104. data/lib/rsmp/site.rb +0 -188
  105. data/lib/rsmp/site_proxy.rb +0 -389
  106. data/lib/rsmp/supervisor.rb +0 -302
  107. data/lib/rsmp/supervisor_proxy.rb +0 -510
  108. data/lib/rsmp/tlc/inputs.rb +0 -134
@@ -0,0 +1,88 @@
1
+ module RSMP
2
+ # RSMP component base class.
3
+ class ComponentBase
4
+ include Inspect
5
+
6
+ attr_reader :c_id, :ntsoid, :xnid, :node, :alarms, :statuses,
7
+ :aggregated_status, :aggregated_status_bools, :grouped
8
+
9
+ AGGREGATED_STATUS_KEYS = %i[local_control
10
+ communication_distruption
11
+ high_priority_alarm
12
+ medium_priority_alarm
13
+ low_priority_alarm
14
+ normal
15
+ rest
16
+ not_connected].freeze
17
+
18
+ def initialize(node:, id:, ntsoid: nil, xnid: nil, grouped: false)
19
+ if grouped == false && (ntsoid || xnid)
20
+ raise RSMP::ConfigurationError,
21
+ 'ntsoid and xnid are only allowed for grouped objects'
22
+ end
23
+
24
+ @c_id = id
25
+ @ntsoid = ntsoid
26
+ @xnid = xnid
27
+ @node = node
28
+ @grouped = grouped
29
+ clear_aggregated_status
30
+ @alarms = {}
31
+ end
32
+
33
+ def now
34
+ node.now
35
+ end
36
+
37
+ def clear_alarm_timestamps
38
+ @alarms.each_value(&:clear_timestamp)
39
+ end
40
+
41
+ def get_alarm_state(alarm_code)
42
+ @alarms[alarm_code] ||= RSMP::AlarmState.new component: self, code: alarm_code
43
+ end
44
+
45
+ def clear_aggregated_status
46
+ @aggregated_status = []
47
+ @aggregated_status_bools = Array.new(8, false)
48
+ @aggregated_status_bools[5] = true
49
+ end
50
+
51
+ def log(str, options)
52
+ default = { component: c_id }
53
+ @node.log str, default.merge(options)
54
+ end
55
+
56
+ def set_aggregated_status(status, options = {})
57
+ status = [status] if status.is_a? Symbol
58
+ raise InvalidArgument unless status.is_a? Array
59
+
60
+ input = status & AGGREGATED_STATUS_KEYS
61
+ return unless input != @aggregated_status
62
+
63
+ AGGREGATED_STATUS_KEYS.each_with_index do |key, index|
64
+ @aggregated_status_bools[index] = status.include?(key)
65
+ end
66
+ aggregated_status_changed options
67
+ end
68
+
69
+ def aggregated_status_bools=(status)
70
+ raise InvalidArgument unless status.is_a? Array
71
+ raise InvalidArgument unless status.size == 8
72
+
73
+ return unless status != @aggregated_status_bools
74
+
75
+ @aggregated_status = []
76
+ AGGREGATED_STATUS_KEYS.each_with_index do |key, index|
77
+ on = status[index] == true
78
+ @aggregated_status_bools[index] = on
79
+ @aggregated_status << key if on
80
+ end
81
+ aggregated_status_changed
82
+ end
83
+
84
+ def aggregated_status_changed(options = {})
85
+ @node.aggregated_status_changed self, options
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,75 @@
1
+ module RSMP
2
+ # A proxy to a remote RSMP component.
3
+ class ComponentProxy < ComponentBase
4
+ def initialize(node:, id:, ntsoid: nil, xnid: nil, grouped: false)
5
+ super
6
+ @alarms = {}
7
+ @statuses = {}
8
+ @allow_repeat_updates = {}
9
+ end
10
+
11
+ # allow the next status update to be a repeat value
12
+ def allow_repeat_updates(subscribe_list)
13
+ subscribe_list.each do |item|
14
+ sci = item['sCI']
15
+ n = item['n']
16
+ @allow_repeat_updates[sci] ||= Set.new # Set is like an array, but with no duplicates
17
+ @allow_repeat_updates[sci] << n
18
+ end
19
+ end
20
+
21
+ # Check that were not receiving repeated update values.
22
+ # The check is not performed for item with an update interval.
23
+ def check_repeat_values(message, subscription_list)
24
+ message.attribute('sS').each do |item|
25
+ check_status_item_for_repeats(item, subscription_list)
26
+ end
27
+ end
28
+
29
+ def check_status_item_for_repeats(item, subscription_list)
30
+ status_code = item['sCI']
31
+ status_name = item['n']
32
+ return if update_rate_set?(subscription_list, status_code, status_name)
33
+ return unless should_check_repeats?(subscription_list, status_code, status_name)
34
+
35
+ new_values = { 's' => item['s'], 'q' => item['q'] }
36
+ old_values = @statuses.dig(status_code, status_name)
37
+ raise RSMP::RepeatedStatusError, "no change for #{status_code} '#{status_name}'" if new_values == old_values
38
+ end
39
+
40
+ def update_rate_set?(subscription_list, status_code, status_name)
41
+ urt = subscription_list.dig(c_id, status_code, status_name, 'uRt')
42
+ urt.to_i.positive?
43
+ end
44
+
45
+ def should_check_repeats?(subscription_list, status_code, status_name)
46
+ soc = subscription_list.dig(c_id, status_code, status_name, 'sOc')
47
+ return false if soc == false
48
+ return false if @allow_repeat_updates[status_code]&.include?(status_name)
49
+
50
+ true
51
+ end
52
+
53
+ # Store the latest status update values
54
+ def store_status(message)
55
+ message.attribute('sS').each do |item|
56
+ sci = item['sCI']
57
+ n = item['n']
58
+ s = item['s']
59
+ q = item['q']
60
+ @statuses[sci] ||= {}
61
+ @statuses[sci][n] = { 's' => s, 'q' => q }
62
+
63
+ # once a value is received, don't allow the value to be a repeat
64
+ @allow_repeat_updates[sci]&.delete(n)
65
+ end
66
+ end
67
+
68
+ # handle incoming alarm
69
+ def handle_alarm(message)
70
+ code = message.attribute('aCId')
71
+ alarm = get_alarm_state code
72
+ alarm.update_from_message message
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,62 @@
1
+ module RSMP
2
+ # Things shared between sites and site proxies
3
+ module Components
4
+ attr_reader :components, :main
5
+
6
+ def initialize_components
7
+ @components = {}
8
+ @main = nil
9
+ end
10
+
11
+ def aggregated_status_changed(component, options = {}); end
12
+
13
+ def setup_components(settings)
14
+ return unless settings
15
+
16
+ check_main_component settings
17
+ settings.each_pair do |type, components_by_type|
18
+ next unless components_by_type
19
+
20
+ components_by_type.each_pair do |id, component_settings|
21
+ component_settings ||= {}
22
+ @components[id] = build_component(id: id, type: type, settings: component_settings)
23
+ @main = @components[id] if type == 'main'
24
+ end
25
+ end
26
+ end
27
+
28
+ def check_main_component(settings)
29
+ raise ConfigurationError, 'main component must be defined' unless settings['main'] && settings['main'].size >= 1
30
+ return unless settings['main'].size > 1
31
+
32
+ raise ConfigurationError, "only one main component can be defined, found #{settings['main'].keys.join(', ')}"
33
+ end
34
+
35
+ def add_component(component)
36
+ @components[component.c_id] = component
37
+ end
38
+
39
+ def infer_component_type(component_id)
40
+ raise UnknownComponent, "Component #{component_id} mising and cannot infer type"
41
+ end
42
+
43
+ def find_component(component_id, build: true)
44
+ component = @components[component_id]
45
+ return component if component
46
+
47
+ return unless build
48
+
49
+ inferred_type = infer_component_type component_id
50
+ component = inferred_type.new node: self, id: component_id
51
+ @components[component_id] = component
52
+ class_name = component.class.name.split('::').last
53
+ class_name << ' component' unless %w[Component ComponentProxy].include?(class_name)
54
+ log "Added component #{component_id} with the inferred type #{class_name}", level: :debug
55
+ component
56
+ end
57
+
58
+ def clear_alarm_timestamps
59
+ @components.each_value(&:clear_alarm_timestamps)
60
+ end
61
+ end
62
+ end
@@ -7,180 +7,190 @@ require 'fileutils'
7
7
  module RSMP
8
8
  module Convert
9
9
  module Export
10
+ # Converts SXL (YAML) structures into JSON Schema files.
11
+ # Converts SXL (YAML) structures into JSON Schema files.
10
12
  module JSONSchema
11
-
12
- @@json_options = {
13
+ JSON_OPTIONS = {
13
14
  array_nl: "\n",
14
15
  object_nl: "\n",
15
16
  indent: ' ',
16
17
  space_before: ' ',
17
18
  space: ' '
18
- }
19
+ }.freeze
19
20
 
20
- def self.output_json item
21
- JSON.generate(item,@@json_options)
21
+ def self.output_json(item)
22
+ JSON.generate(item, JSON_OPTIONS)
22
23
  end
23
24
 
24
- def self.build_value item
25
+ def self.build_value(item)
25
26
  out = {}
26
-
27
- if item['description']
28
- out["description"] = item['description']
29
- end
27
+ out['description'] = item['description'] if item['description']
30
28
 
31
29
  if item['list']
32
- case item['type']
33
- when "boolean"
34
- out["$ref"] = "../../../core/3.1.1/definitions.json#/boolean_list"
35
- when "integer", "ordinal", "unit", "scale", "long"
36
- out["$ref"] = "../../../core/3.1.1/definitions.json#/integer_list"
37
- else
38
- raise "Error: List of #{item['type']} is not supported: #{item.inspect}"
39
- end
30
+ build_list_value(out, item)
31
+ else
32
+ build_single_value(out, item)
33
+ end
40
34
 
41
- if item["values"]
42
- value_list = item["values"].keys.join('|')
43
- out['pattern'] = /(?-mix:^(#{value_list})(?:,(#{value_list}))*$)/
44
- end
35
+ out
36
+ end
45
37
 
46
- if item["pattern"]
47
- puts "Warning: Pattern not support for lists: #{item.inspect}"
48
- end
38
+ def self.build_list_value(out, item)
39
+ case item['type']
40
+ when 'boolean'
41
+ out['$ref'] = '../../../core/3.1.1/definitions.json#/boolean_list'
42
+ when 'integer', 'ordinal', 'unit', 'scale', 'long'
43
+ out['$ref'] = '../../../core/3.1.1/definitions.json#/integer_list'
49
44
  else
50
- case item['type']
51
- when "string", "base64"
52
- out["type"] = "string"
53
- when "boolean"
54
- out["$ref"] = "../../../core/3.1.1/definitions.json#/boolean"
55
- when "timestamp"
56
- out["$ref"] = "../../../core/3.1.1/definitions.json#/timestamp"
57
- when "integer", "ordinal", "unit", "scale", "long"
58
- out["$ref"] = "../../../core/3.1.1/definitions.json#/integer"
59
- else
60
- out["type"] = "string"
61
- end
62
-
63
- if item["values"]
64
- out["enum"] = item["values"].keys.sort
65
- end
45
+ raise "Error: List of #{item['type']} is not supported: #{item.inspect}"
46
+ end
66
47
 
67
- if item["pattern"]
68
- out["pattern"] = item["pattern"]
69
- end
48
+ if item['values']
49
+ value_list = item['values'].keys.join('|')
50
+ out['pattern'] = /(?-mix:^(#{value_list})(?:,(#{value_list}))*$)/
70
51
  end
71
52
 
53
+ puts "Warning: Pattern not support for lists: #{item.inspect}" if item['pattern']
54
+ end
55
+
56
+ def self.build_single_value(out, item)
57
+ case item['type']
58
+ when 'boolean'
59
+ out['$ref'] = '../../../core/3.1.1/definitions.json#/boolean'
60
+ when 'timestamp'
61
+ out['$ref'] = '../../../core/3.1.1/definitions.json#/timestamp'
62
+ when 'integer', 'ordinal', 'unit', 'scale', 'long'
63
+ out['$ref'] = '../../../core/3.1.1/definitions.json#/integer'
64
+ else # includes 'string', 'base64' and any other types
65
+ out['type'] = 'string'
66
+ end
72
67
 
73
- out
68
+ out['enum'] = item['values'].keys.sort if item['values']
69
+ out['pattern'] = item['pattern'] if item['pattern']
74
70
  end
75
71
 
76
- def self.build_item item, property_key: 'v'
77
- json = { "allOf" => [ { "description" => item['description'] } ] }
72
+ def self.build_item(item, property_key: 'v')
73
+ json = { 'allOf' => [{ 'description' => item['description'] }] }
78
74
  if item['arguments']
79
- json["allOf"].first["properties"] = { "n" => { "enum" => item['arguments'].keys.sort } }
80
- item['arguments'].each_pair do |key,argument|
81
- json["allOf"] << {
82
- "if" => { "required" => ["n"], "properties" => { "n" => { "const" => key }}},
83
- "then" => { "properties" => { property_key => build_value(argument) } }
75
+ json['allOf'].first['properties'] = { 'n' => { 'enum' => item['arguments'].keys.sort } }
76
+ item['arguments'].each_pair do |key, argument|
77
+ json['allOf'] << {
78
+ 'if' => { 'required' => ['n'], 'properties' => { 'n' => { 'const' => key } } },
79
+ 'then' => { 'properties' => { property_key => build_value(argument) } }
84
80
  }
85
81
  end
86
82
  end
87
83
  json
88
84
  end
89
85
 
90
- def self.output_alarms out, items
86
+ def self.output_alarms(out, items)
91
87
  list = items.keys.sort.map do |key|
92
88
  {
93
- "if" => { "required" => ["aCId"], "properties" => { "aCId" => { "const" => key }}},
94
- "then" => { "$ref" => "#{key}.json" }
89
+ 'if' => { 'required' => ['aCId'], 'properties' => { 'aCId' => { 'const' => key } } },
90
+ 'then' => { '$ref' => "#{key}.json" }
95
91
  }
96
92
  end
97
93
  json = {
98
- "properties" => {
99
- "aCId" => { "enum" => items.keys.sort },
100
- "rvs" => { "items" => { "allOf" => list } }
94
+ 'properties' => {
95
+ 'aCId' => { 'enum' => items.keys.sort },
96
+ 'rvs' => { 'items' => { 'allOf' => list } }
101
97
  }
102
98
  }
103
99
  out['alarms/alarms.json'] = output_json json
104
- items.each_pair { |key,item| output_alarm out, key, item }
100
+ items.each_pair { |key, item| output_alarm out, key, item }
105
101
  end
106
102
 
107
- def self.output_alarm out, key, item
103
+ def self.output_alarm(out, key, item)
108
104
  json = build_item item
109
105
  out["alarms/#{key}.json"] = output_json json
110
106
  end
111
107
 
112
- def self.output_statuses out, items
113
- list = [ { "properties" => { "sCI" => { "enum"=> items.keys.sort }}} ]
108
+ def self.output_statuses(out, items)
109
+ list = [{ 'properties' => { 'sCI' => { 'enum' => items.keys.sort } } }]
114
110
  items.keys.sort.each do |key|
115
111
  list << {
116
- "if"=> { "required" => ["sCI"], "properties" => { "sCI"=> { "const"=> key }}},
117
- "then" => { "$ref" => "#{key}.json" }
112
+ 'if' => { 'required' => ['sCI'], 'properties' => { 'sCI' => { 'const' => key } } },
113
+ 'then' => { '$ref' => "#{key}.json" }
118
114
  }
119
115
  end
120
- json = { "properties" => { "sS" => { "items" => { "allOf" => list }}}}
116
+ json = { 'properties' => { 'sS' => { 'items' => { 'allOf' => list } } } }
121
117
  out['statuses/statuses.json'] = output_json json
122
- items.each_pair { |key,item| output_status out, key, item }
118
+ items.each_pair { |key, item| output_status out, key, item }
123
119
  end
124
120
 
125
- def self.output_status out, key, item
126
- json = build_item item, property_key:'s'
121
+ def self.output_status(out, key, item)
122
+ json = build_item item, property_key: 's'
127
123
  out["statuses/#{key}.json"] = output_json json
128
124
  end
129
125
 
130
- def self.output_commands out, items
131
- list = [ { "properties" => { "cCI" => { "enum"=> items.keys.sort }}} ]
126
+ def self.output_commands(out, items)
127
+ list = [{ 'properties' => { 'cCI' => { 'enum' => items.keys.sort } } }]
132
128
  items.keys.sort.each do |key|
133
129
  list << {
134
- "if" => { "required" => ["cCI"], "properties" => { "cCI"=> { "const"=> key }}},
135
- "then" => { "$ref" => "#{key}.json" }
130
+ 'if' => { 'required' => ['cCI'], 'properties' => { 'cCI' => { 'const' => key } } },
131
+ 'then' => { '$ref' => "#{key}.json" }
136
132
  }
137
133
  end
138
- json = { "items" => { "allOf" => list }}
134
+ json = { 'items' => { 'allOf' => list } }
139
135
  out['commands/commands.json'] = output_json json
140
136
 
141
- json = { "properties" => { "arg" => { "$ref" => "commands.json" }}}
137
+ json = { 'properties' => { 'arg' => { '$ref' => 'commands.json' } } }
142
138
  out['commands/command_requests.json'] = output_json json
143
139
 
144
- json = { "properties" => { "rvs" => { "$ref" => "commands.json" }}}
140
+ json = { 'properties' => { 'rvs' => { '$ref' => 'commands.json' } } }
145
141
  out['commands/command_responses.json'] = output_json json
146
142
 
147
- items.each_pair { |key,item| output_command out, key, item }
143
+ items.each_pair { |key, item| output_command out, key, item }
148
144
  end
149
145
 
150
- def self.output_command out, key, item
146
+ def self.output_command(out, key, item)
151
147
  json = build_item item
152
- json["allOf"].first["properties"]['cO'] = { "const" => item['command'] }
148
+ json['allOf'].first['properties']['cO'] = { 'const' => item['command'] }
153
149
  out["commands/#{key}.json"] = output_json json
154
150
  end
155
151
 
156
- def self.output_root out, meta
157
- json = {
158
- "name"=> meta['name'],
159
- "description"=> meta['description'],
160
- "version"=> meta['version'],
161
- "allOf" => [
162
- {
163
- "if" => { "required" => ["type"], "properties" => { "type" => { "const" => "CommandRequest" }}},
164
- "then" => { "$ref" => "commands/command_requests.json" }
165
- },
166
- {
167
- "if" => { "required" => ["type"], "properties" => { "type" => { "const" => "CommandResponse" }}},
168
- "then" => { "$ref" => "commands/command_responses.json" }
169
- },
170
- {
171
- "if" => { "required" => ["type"], "properties" => { "type" => { "enum" => ["StatusRequest","StatusResponse","StatusSubscribe","StatusUnsubscribe","StatusUpdate"] }}},
172
- "then" => { "$ref" => "statuses/statuses.json" }
173
- },
174
- {
175
- "if" => { "required" => ["type"], "properties" => { "type" => { "const" => "Alarm" }}},
176
- "then" => { "$ref" => "alarms/alarms.json" }
177
- }
178
- ]
152
+ def self.build_root_schema(meta)
153
+ {
154
+ 'name' => meta['name'],
155
+ 'description' => meta['description'],
156
+ 'version' => meta['version'],
157
+ 'allOf' => build_root_refs
179
158
  }
180
- out["sxl.json"] = output_json json
181
159
  end
182
160
 
183
- def self.generate sxl
161
+ def self.build_root_refs
162
+ [
163
+ {
164
+ 'if' => { 'required' => ['type'], 'properties' => { 'type' => { 'const' => 'CommandRequest' } } },
165
+ 'then' => { '$ref' => 'commands/command_requests.json' }
166
+ },
167
+ {
168
+ 'if' => { 'required' => ['type'], 'properties' => { 'type' => { 'const' => 'CommandResponse' } } },
169
+ 'then' => { '$ref' => 'commands/command_responses.json' }
170
+ },
171
+ {
172
+ 'if' => { 'required' => ['type'],
173
+ 'properties' => {
174
+ 'type' => {
175
+ 'enum' => %w[StatusRequest StatusResponse StatusSubscribe
176
+ StatusUnsubscribe StatusUpdate]
177
+ }
178
+ } },
179
+ 'then' => { '$ref' => 'statuses/statuses.json' }
180
+ },
181
+ {
182
+ 'if' => { 'required' => ['type'], 'properties' => { 'type' => { 'const' => 'Alarm' } } },
183
+ 'then' => { '$ref' => 'alarms/alarms.json' }
184
+ }
185
+ ]
186
+ end
187
+
188
+ def self.output_root(out, meta)
189
+ json = build_root_schema(meta)
190
+ out['sxl.json'] = output_json json
191
+ end
192
+
193
+ def self.generate(sxl)
184
194
  out = {}
185
195
  output_root out, sxl[:meta]
186
196
  output_alarms out, sxl[:alarms]
@@ -189,18 +199,16 @@ module RSMP
189
199
  out
190
200
  end
191
201
 
192
- def self.write sxl, folder
202
+ def self.write(sxl, folder)
193
203
  out = generate sxl
194
- out.each_pair do |relative_path,str|
204
+ out.each_pair do |relative_path, str|
195
205
  path = File.join(folder, relative_path)
196
- FileUtils.mkdir_p File.dirname(path) # create folders if needed
197
- file = File.open(path, 'w+') # w+ means truncate or create new file
206
+ FileUtils.mkdir_p File.dirname(path) # create folders if needed
207
+ file = File.open(path, 'w+') # w+ means truncate or create new file
198
208
  file.puts str
199
209
  end
200
210
  end
201
-
202
211
  end
203
-
204
212
  end
205
213
  end
206
- end
214
+ end
@@ -7,35 +7,39 @@ require 'fileutils'
7
7
  module RSMP
8
8
  module Convert
9
9
  module Import
10
+ # Importer for SXL in YAML format.
10
11
  module YAML
11
-
12
- def self.read path
12
+ def self.read(path)
13
13
  convert ::YAML.load_file(path)
14
14
  end
15
15
 
16
- def self.parse str
16
+ def self.parse(str)
17
17
  convert ::YAML.load(str)
18
18
  end
19
19
 
20
- def self.convert yaml
21
- sxl = {
22
- meta: {},
23
- alarms: {},
24
- statuses: {},
25
- commands: {}
26
- }
27
-
20
+ def self.convert(yaml)
21
+ sxl = build_empty_sxl
28
22
  sxl[:meta] = yaml['meta']
23
+ merge_objects(sxl, yaml['objects'])
24
+ sxl
25
+ end
26
+
27
+ def self.build_empty_sxl
28
+ { meta: {}, alarms: {}, statuses: {}, commands: {} }
29
+ end
29
30
 
30
- yaml['objects'].each_pair do |type,object|
31
- object["alarms"].each { |id,item| sxl[:alarms][id] = item } if object["alarms"]
32
- object["statuses"].each { |id,item| sxl[:statuses][id] = item } if object["statuses"]
33
- object["commands"].each { |id,item| sxl[:commands][id] = item } if object["commands"]
31
+ def self.merge_objects(sxl, objects)
32
+ objects.each_pair do |_type, object|
33
+ merge_object_items(sxl, object)
34
34
  end
35
- sxl
36
35
  end
37
- end
38
36
 
37
+ def self.merge_object_items(sxl, object)
38
+ object['alarms']&.each { |id, item| sxl[:alarms][id] = item }
39
+ object['statuses']&.each { |id, item| sxl[:statuses][id] = item }
40
+ object['commands']&.each { |id, item| sxl[:commands][id] = item }
41
+ end
42
+ end
39
43
  end
40
44
  end
41
- end
45
+ end
@@ -1,12 +1,9 @@
1
- # Get the current time in UTC, with optional adjustment
2
- # Convertion to string uses the RSMP format 2015-06-08T12:01:39.654Z
3
- # Note that using to_s on a my_clock.to_s will not produce an RSMP formatted timestamp,
4
- # you need to use Clock.to_s my_clock
5
-
6
1
  require 'time'
7
2
 
8
3
  module RSMP
9
-
4
+ # Get the current time in UTC, with optional adjustment.
5
+ # Conversion to string uses the RSMP format 2015-06-08T12:01:39.654Z
6
+ # Use `Clock.to_s` to format times in RSMP format.
10
7
  class Clock
11
8
  attr_reader :adjustment
12
9
 
@@ -14,7 +11,7 @@ module RSMP
14
11
  @adjustment = 0
15
12
  end
16
13
 
17
- def set target
14
+ def set(target)
18
15
  @adjustment = target - Time.now
19
16
  end
20
17
 
@@ -34,12 +31,12 @@ module RSMP
34
31
  Time.now.utc
35
32
  end
36
33
 
37
- def self.to_s time=nil
38
- (time || now).strftime("%FT%T.%3NZ")
34
+ def self.to_s(time = nil)
35
+ (time || now).strftime('%FT%T.%3NZ')
39
36
  end
40
37
 
41
- def self.parse str
38
+ def self.parse(str)
42
39
  Time.parse(str)
43
40
  end
44
41
  end
45
- end
42
+ end
@@ -1,7 +1,9 @@
1
+ # Extensions to Hash providing a `deep_merge` helper.
1
2
  class Hash
2
3
  def deep_merge(other_hash)
3
4
  return self unless other_hash
4
- self.merge(other_hash) do |key, old, fresh|
5
+
6
+ merge(other_hash) do |_key, old, fresh|
5
7
  if old.is_a?(Hash) && fresh.is_a?(Hash)
6
8
  old.deep_merge(fresh)
7
9
  else