rsmp 0.37.0 → 0.38.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 (106) 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 +80 -0
  6. data/Gemfile +13 -1
  7. data/Gemfile.lock +34 -1
  8. data/Rakefile +3 -3
  9. data/lib/rsmp/cli.rb +147 -124
  10. data/lib/rsmp/collect/ack_collector.rb +8 -7
  11. data/lib/rsmp/collect/aggregated_status_collector.rb +4 -4
  12. data/lib/rsmp/collect/alarm_collector.rb +31 -23
  13. data/lib/rsmp/collect/alarm_matcher.rb +3 -3
  14. data/lib/rsmp/collect/collector/logging.rb +17 -0
  15. data/lib/rsmp/collect/collector/reporting.rb +44 -0
  16. data/lib/rsmp/collect/collector/status.rb +34 -0
  17. data/lib/rsmp/collect/collector.rb +69 -150
  18. data/lib/rsmp/collect/command_matcher.rb +19 -6
  19. data/lib/rsmp/collect/command_response_collector.rb +7 -7
  20. data/lib/rsmp/collect/distributor.rb +14 -11
  21. data/lib/rsmp/collect/filter.rb +31 -15
  22. data/lib/rsmp/collect/matcher.rb +7 -11
  23. data/lib/rsmp/collect/queue.rb +4 -4
  24. data/lib/rsmp/collect/receiver.rb +10 -12
  25. data/lib/rsmp/collect/state_collector.rb +116 -77
  26. data/lib/rsmp/collect/status_collector.rb +6 -6
  27. data/lib/rsmp/collect/status_matcher.rb +17 -7
  28. data/lib/rsmp/{alarm_state.rb → component/alarm_state.rb} +76 -37
  29. data/lib/rsmp/{component.rb → component/component.rb} +15 -15
  30. data/lib/rsmp/component/component_base.rb +89 -0
  31. data/lib/rsmp/component/component_proxy.rb +75 -0
  32. data/lib/rsmp/component/components.rb +63 -0
  33. data/lib/rsmp/convert/export/json_schema.rb +116 -110
  34. data/lib/rsmp/convert/import/yaml.rb +21 -18
  35. data/lib/rsmp/{rsmp.rb → helpers/clock.rb} +5 -6
  36. data/lib/rsmp/{deep_merge.rb → helpers/deep_merge.rb} +2 -1
  37. data/lib/rsmp/helpers/error.rb +71 -0
  38. data/lib/rsmp/{inspect.rb → helpers/inspect.rb} +6 -10
  39. data/lib/rsmp/log/archive.rb +98 -0
  40. data/lib/rsmp/log/colorization.rb +41 -0
  41. data/lib/rsmp/log/filtering.rb +54 -0
  42. data/lib/rsmp/log/logger.rb +206 -0
  43. data/lib/rsmp/{logging.rb → log/logging.rb} +5 -7
  44. data/lib/rsmp/message.rb +159 -148
  45. data/lib/rsmp/{node.rb → node/node.rb} +19 -17
  46. data/lib/rsmp/{protocol.rb → node/protocol.rb} +5 -3
  47. data/lib/rsmp/node/site/site.rb +195 -0
  48. data/lib/rsmp/node/supervisor/modules/configuration.rb +59 -0
  49. data/lib/rsmp/node/supervisor/modules/connection.rb +140 -0
  50. data/lib/rsmp/node/supervisor/modules/sites.rb +64 -0
  51. data/lib/rsmp/node/supervisor/supervisor.rb +72 -0
  52. data/lib/rsmp/{task.rb → node/task.rb} +12 -14
  53. data/lib/rsmp/proxy/modules/acknowledgements.rb +144 -0
  54. data/lib/rsmp/proxy/modules/receive.rb +119 -0
  55. data/lib/rsmp/proxy/modules/send.rb +76 -0
  56. data/lib/rsmp/proxy/modules/state.rb +25 -0
  57. data/lib/rsmp/proxy/modules/tasks.rb +105 -0
  58. data/lib/rsmp/proxy/modules/versions.rb +69 -0
  59. data/lib/rsmp/proxy/modules/watchdogs.rb +66 -0
  60. data/lib/rsmp/proxy/proxy.rb +199 -0
  61. data/lib/rsmp/proxy/site/modules/aggregated_status.rb +52 -0
  62. data/lib/rsmp/proxy/site/modules/alarms.rb +27 -0
  63. data/lib/rsmp/proxy/site/modules/commands.rb +31 -0
  64. data/lib/rsmp/proxy/site/modules/status.rb +110 -0
  65. data/lib/rsmp/proxy/site/site_proxy.rb +205 -0
  66. data/lib/rsmp/proxy/supervisor/modules/aggregated_status.rb +47 -0
  67. data/lib/rsmp/proxy/supervisor/modules/alarms.rb +73 -0
  68. data/lib/rsmp/proxy/supervisor/modules/commands.rb +53 -0
  69. data/lib/rsmp/proxy/supervisor/modules/status.rb +204 -0
  70. data/lib/rsmp/proxy/supervisor/supervisor_proxy.rb +178 -0
  71. data/lib/rsmp/tlc/detector_logic.rb +18 -34
  72. data/lib/rsmp/tlc/input_states.rb +126 -0
  73. data/lib/rsmp/tlc/modules/detector_logics.rb +50 -0
  74. data/lib/rsmp/tlc/modules/display.rb +78 -0
  75. data/lib/rsmp/tlc/modules/helpers.rb +41 -0
  76. data/lib/rsmp/tlc/modules/inputs.rb +173 -0
  77. data/lib/rsmp/tlc/modules/modes.rb +253 -0
  78. data/lib/rsmp/tlc/modules/outputs.rb +30 -0
  79. data/lib/rsmp/tlc/modules/plans.rb +218 -0
  80. data/lib/rsmp/tlc/modules/signal_groups.rb +109 -0
  81. data/lib/rsmp/tlc/modules/startup_sequence.rb +22 -0
  82. data/lib/rsmp/tlc/modules/system.rb +140 -0
  83. data/lib/rsmp/tlc/modules/traffic_data.rb +49 -0
  84. data/lib/rsmp/tlc/signal_group.rb +37 -41
  85. data/lib/rsmp/tlc/signal_plan.rb +14 -11
  86. data/lib/rsmp/tlc/signal_priority.rb +39 -35
  87. data/lib/rsmp/tlc/startup_sequence.rb +59 -0
  88. data/lib/rsmp/tlc/traffic_controller.rb +38 -1010
  89. data/lib/rsmp/tlc/traffic_controller_site.rb +58 -57
  90. data/lib/rsmp/version.rb +1 -1
  91. data/lib/rsmp.rb +82 -48
  92. data/rsmp.gemspec +24 -31
  93. metadata +79 -139
  94. data/lib/rsmp/archive.rb +0 -76
  95. data/lib/rsmp/collect/message_matchers.rb +0 -0
  96. data/lib/rsmp/component_base.rb +0 -87
  97. data/lib/rsmp/component_proxy.rb +0 -57
  98. data/lib/rsmp/components.rb +0 -65
  99. data/lib/rsmp/error.rb +0 -71
  100. data/lib/rsmp/logger.rb +0 -216
  101. data/lib/rsmp/proxy.rb +0 -693
  102. data/lib/rsmp/site.rb +0 -188
  103. data/lib/rsmp/site_proxy.rb +0 -389
  104. data/lib/rsmp/supervisor.rb +0 -302
  105. data/lib/rsmp/supervisor_proxy.rb +0 -510
  106. data/lib/rsmp/tlc/inputs.rb +0 -134
@@ -1,5 +1,4 @@
1
1
  module RSMP
2
-
3
2
  # The state of an alarm on a component.
4
3
  # The alarm state is for a particular alarm code,
5
4
  # a component typically have an alarm state for each
@@ -8,10 +7,8 @@ module RSMP
8
7
  class AlarmState
9
8
  attr_reader :component_id, :code, :acknowledged, :suspended, :active, :timestamp, :category, :priority, :rvs
10
9
 
11
- def self.create_from_message component, message
12
- self.new(
13
- component: component,
14
- code: message.attribute("aCId"),
10
+ def self.create_from_message(component, message)
11
+ options = {
15
12
  timestamp: RSMP::Clock.parse(message.attribute('aTs')),
16
13
  acknowledged: message.attribute('ack') == 'Acknowledged',
17
14
  suspended: message.attribute('aS') == 'Suspended',
@@ -19,22 +16,21 @@ module RSMP
19
16
  category: message.attribute('cat'),
20
17
  priority: message.attribute('pri').to_i,
21
18
  rvs: message.attribute('rvs')
22
- )
19
+ }
20
+ new(component: component, code: message.attribute('aCId'), **options)
23
21
  end
24
22
 
25
- def initialize component:, code:,
26
- suspended: false, acknowledged: false, active: false, timestamp: nil,
27
- category: 'D', priority: 2, rvs: []
23
+ def initialize(component:, code:, **options)
28
24
  @component = component
29
25
  @component_id = component.c_id
30
26
  @code = code
31
- @suspended = !!suspended
32
- @acknowledged = !!acknowledged
33
- @active = !!active
34
- @timestamp = timestamp
35
- @category = category || 'D'
36
- @priority = priority || 2
37
- @rvs = rvs
27
+ @suspended = !!options[:suspended]
28
+ @acknowledged = !!options[:acknowledged]
29
+ @active = !!options[:active]
30
+ @timestamp = options[:timestamp]
31
+ @category = options[:category] || 'D'
32
+ @priority = options[:priority] || 2
33
+ @rvs = options[:rvs] || []
38
34
  end
39
35
 
40
36
  def to_hash
@@ -52,19 +48,22 @@ module RSMP
52
48
  end
53
49
 
54
50
  def acknowledge
55
- change, @acknowledged = !@acknowledged, true
51
+ change = !@acknowledged
52
+ @acknowledged = true
56
53
  update_timestamp if change
57
54
  change
58
55
  end
59
56
 
60
57
  def suspend
61
- change, @suspended = !@suspended, true
58
+ change = !@suspended
59
+ @suspended = true
62
60
  update_timestamp if change
63
61
  change
64
62
  end
65
63
 
66
64
  def resume
67
- change, @suspended = @suspended, false
65
+ change = @suspended
66
+ @suspended = false
68
67
  update_timestamp if change
69
68
  change
70
69
  end
@@ -73,29 +72,33 @@ module RSMP
73
72
  # is when it's activated. See:
74
73
  # https://rsmp-nordic.org/rsmp_specifications/core/3.2.0/applicability/basic_structure.html#alarm-status
75
74
  def activate
76
- change, @active, @acknowledged = !@active, true, false
75
+ change = !@active
76
+ @active = true
77
+ @acknowledged = false
77
78
  update_timestamp if change
78
79
  change
79
80
  end
80
81
 
81
82
  def deactivate
82
- change, @active = @active, false
83
+ change = @active
84
+ @active = false
83
85
  update_timestamp if change
84
86
  change
85
87
  end
86
-
88
+
87
89
  def update_timestamp
88
90
  @timestamp = @component.now
89
91
  end
90
92
 
91
- def differ_from_message? message
92
- return true if RSMP::Clock.to_s(@timestamp) != message.attribute('aTs')
93
- return true if message.attribute('ack') && @acknowledged != (message.attribute('ack').downcase == 'acknowledged')
94
- return true if message.attribute('sS') && @suspended != (message.attribute('sS').downcase == 'suspended')
95
- return true if message.attribute('aS') && @active != (message.attribute('aS').downcase == 'active')
96
- return true if message.attribute('cat') && @category != message.attribute('cat')
97
- return true if message.attribute('pri') && @priority != message.attribute('pri').to_i
98
- #return true @rvs = message.attribute('rvs')
93
+ def differ_from_message?(message)
94
+ return true if timestamp_differs?(message)
95
+ return true if acknowledgment_differs?(message)
96
+ return true if suspension_differs?(message)
97
+ return true if activity_differs?(message)
98
+ return true if category_differs?(message)
99
+ return true if priority_differs?(message)
100
+
101
+ # return true @rvs = message.attribute('rvs')
99
102
  false
100
103
  end
101
104
 
@@ -103,20 +106,20 @@ module RSMP
103
106
  @timestamp = nil
104
107
  end
105
108
 
106
- def older_message? message
107
- return false if @timestamp == nil
109
+ def older_message?(message)
110
+ return false if @timestamp.nil?
111
+
108
112
  RSMP::Clock.parse(message.attribute('aTs')) < @timestamp
109
113
  end
110
114
 
111
115
  # update from rsmp message
112
116
  # component id, alarm code and specialization are not updated
113
- def update_from_message message
117
+ def update_from_message(message)
114
118
  unless differ_from_message? message
115
- raise RepeatedAlarmError.new("no changes from previous alarm #{message.m_id_short}")
116
- end
117
- if older_message? message
118
- raise TimestampError.new("timestamp is earlier than previous alarm #{message.m_id_short}")
119
+ raise RepeatedAlarmError,
120
+ "no changes from previous alarm #{message.m_id_short}"
119
121
  end
122
+ raise TimestampError, "timestamp is earlier than previous alarm #{message.m_id_short}" if older_message? message
120
123
  ensure
121
124
  @timestamp = RSMP::Clock.parse message.attribute('aTs')
122
125
  @acknowledged = message.attribute('ack') == 'True'
@@ -126,5 +129,41 @@ module RSMP
126
129
  @priority = message.attribute('pri').to_i
127
130
  @rvs = message.attribute('rvs')
128
131
  end
132
+
133
+ private
134
+
135
+ def timestamp_differs?(message)
136
+ RSMP::Clock.to_s(@timestamp) != message.attribute('aTs')
137
+ end
138
+
139
+ def acknowledgment_differs?(message)
140
+ return false unless message.attribute('ack')
141
+
142
+ @acknowledged != (message.attribute('ack').downcase == 'acknowledged')
143
+ end
144
+
145
+ def suspension_differs?(message)
146
+ return false unless message.attribute('sS')
147
+
148
+ @suspended != (message.attribute('sS').downcase == 'suspended')
149
+ end
150
+
151
+ def activity_differs?(message)
152
+ return false unless message.attribute('aS')
153
+
154
+ @active != (message.attribute('aS').downcase == 'active')
155
+ end
156
+
157
+ def category_differs?(message)
158
+ return false unless message.attribute('cat')
159
+
160
+ @category != message.attribute('cat')
161
+ end
162
+
163
+ def priority_differs?(message)
164
+ return false unless message.attribute('pri')
165
+
166
+ @priority != message.attribute('pri').to_i
167
+ end
129
168
  end
130
169
  end
@@ -1,19 +1,19 @@
1
1
  module RSMP
2
2
  # RSMP component
3
3
  class Component < ComponentBase
4
- def initialize node:, id:, ntsOId: nil, xNId: nil, grouped: false
4
+ def initialize(node:, id:, ntsoid: nil, xnid: nil, grouped: false)
5
5
  super
6
6
  end
7
7
 
8
- def handle_command command_code, arg
9
- raise UnknownCommand.new "Command #{command_code} not implemented by #{self.class}"
8
+ def handle_command(command_code, _arg)
9
+ raise UnknownCommand, "Command #{command_code} not implemented by #{self.class}"
10
10
  end
11
11
 
12
- def get_status status_code, status_name=nil, options={}
13
- raise UnknownStatus.new "Status #{status_code}/#{status_name} not implemented by #{self.class}"
12
+ def get_status(status_code, status_name = nil, _options = {})
13
+ raise UnknownStatus, "Status #{status_code}/#{status_name} not implemented by #{self.class}"
14
14
  end
15
15
 
16
- def acknowledge_alarm alarm_code
16
+ def acknowledge_alarm(alarm_code)
17
17
  alarm = get_alarm_state alarm_code
18
18
  if alarm.acknowledge
19
19
  log "Acknowledging alarm #{alarm_code}", level: :info
@@ -23,17 +23,17 @@ module RSMP
23
23
  end
24
24
  end
25
25
 
26
- def suspend_alarm alarm_code
26
+ def suspend_alarm(alarm_code)
27
27
  alarm = get_alarm_state alarm_code
28
28
  if alarm.suspend
29
29
  log "Suspending alarm #{alarm_code}", level: :info
30
30
  @node.alarm_suspended_or_resumed alarm
31
31
  else
32
32
  log "Alarm #{alarm_code} already suspended", level: :info
33
- end
33
+ end
34
34
  end
35
35
 
36
- def resume_alarm alarm_code
36
+ def resume_alarm(alarm_code)
37
37
  alarm = get_alarm_state alarm_code
38
38
  if alarm.resume
39
39
  log "Resuming alarm #{alarm_code}", level: :info
@@ -43,22 +43,22 @@ module RSMP
43
43
  end
44
44
  end
45
45
 
46
- def activate_alarm alarm_code
46
+ def activate_alarm(alarm_code)
47
47
  alarm = get_alarm_state alarm_code
48
48
  return unless alarm.activate
49
+
49
50
  log "Activating alarm #{alarm_code}", level: :info
50
51
  @node.alarm_activated_or_deactivated alarm
51
52
  end
52
53
 
53
- def deactivate_alarm alarm_code
54
+ def deactivate_alarm(alarm_code)
54
55
  alarm = get_alarm_state alarm_code
55
56
  return unless alarm.deactivate
57
+
56
58
  log "Deactivating alarm #{alarm_code}", level: :info
57
59
  @node.alarm_activated_or_deactivated alarm
58
60
  end
59
61
 
60
- def status_updates_sent
61
- end
62
-
62
+ def status_updates_sent; end
63
63
  end
64
- end
64
+ end
@@ -0,0 +1,89 @@
1
+ module RSMP
2
+ # RSMP component base class.
3
+
4
+ class ComponentBase
5
+ include Inspect
6
+
7
+ attr_reader :c_id, :ntsoid, :xnid, :node, :alarms, :statuses,
8
+ :aggregated_status, :aggregated_status_bools, :grouped
9
+
10
+ AGGREGATED_STATUS_KEYS = %i[local_control
11
+ communication_distruption
12
+ high_priority_alarm
13
+ medium_priority_alarm
14
+ low_priority_alarm
15
+ normal
16
+ rest
17
+ not_connected].freeze
18
+
19
+ def initialize(node:, id:, ntsoid: nil, xnid: nil, grouped: false)
20
+ if grouped == false && (ntsoid || xnid)
21
+ raise RSMP::ConfigurationError,
22
+ 'ntsoid and xnid are only allowed for grouped objects'
23
+ end
24
+
25
+ @c_id = id
26
+ @ntsoid = ntsoid
27
+ @xnid = xnid
28
+ @node = node
29
+ @grouped = grouped
30
+ clear_aggregated_status
31
+ @alarms = {}
32
+ end
33
+
34
+ def now
35
+ node.now
36
+ end
37
+
38
+ def clear_alarm_timestamps
39
+ @alarms.each_value(&:clear_timestamp)
40
+ end
41
+
42
+ def get_alarm_state(alarm_code)
43
+ @alarms[alarm_code] ||= RSMP::AlarmState.new component: self, code: alarm_code
44
+ end
45
+
46
+ def clear_aggregated_status
47
+ @aggregated_status = []
48
+ @aggregated_status_bools = Array.new(8, false)
49
+ @aggregated_status_bools[5] = true
50
+ end
51
+
52
+ def log(str, options)
53
+ default = { component: c_id }
54
+ @node.log str, default.merge(options)
55
+ end
56
+
57
+ def set_aggregated_status(status, options = {})
58
+ status = [status] if status.is_a? Symbol
59
+ raise InvalidArgument unless status.is_a? Array
60
+
61
+ input = status & AGGREGATED_STATUS_KEYS
62
+ return unless input != @aggregated_status
63
+
64
+ AGGREGATED_STATUS_KEYS.each_with_index do |key, index|
65
+ @aggregated_status_bools[index] = status.include?(key)
66
+ end
67
+ aggregated_status_changed options
68
+ end
69
+
70
+ def aggregated_status_bools=(status)
71
+ raise InvalidArgument unless status.is_a? Array
72
+ raise InvalidArgument unless status.size == 8
73
+
74
+ return unless status != @aggregated_status_bools
75
+
76
+ @aggregated_status = []
77
+ AGGREGATED_STATUS_KEYS.each_with_index do |key, index|
78
+ on = status[index] == true
79
+ @aggregated_status_bools[index] = on
80
+ @aggregated_status << key if on
81
+ end
82
+ aggregated_status_changed
83
+ end
84
+
85
+ def aggregated_status_changed(options = {})
86
+ @node.aggregated_status_changed self, options
87
+ end
88
+ end
89
+ 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,63 @@
1
+ # Things shared between sites and site proxies
2
+
3
+ module RSMP
4
+ module Components
5
+ attr_reader :components, :main
6
+
7
+ def initialize_components
8
+ @components = {}
9
+ @main = nil
10
+ end
11
+
12
+ def aggregated_status_changed(component, options = {}); end
13
+
14
+ def setup_components(settings)
15
+ return unless settings
16
+
17
+ check_main_component settings
18
+ settings.each_pair do |type, components_by_type|
19
+ next unless components_by_type
20
+
21
+ components_by_type.each_pair do |id, component_settings|
22
+ component_settings ||= {}
23
+ @components[id] = build_component(id: id, type: type, settings: component_settings)
24
+ @main = @components[id] if type == 'main'
25
+ end
26
+ end
27
+ end
28
+
29
+ def check_main_component(settings)
30
+ raise ConfigurationError, 'main component must be defined' unless settings['main'] && settings['main'].size >= 1
31
+ return unless settings['main'].size > 1
32
+
33
+ raise ConfigurationError, "only one main component can be defined, found #{settings['main'].keys.join(', ')}"
34
+ end
35
+
36
+ def add_component(component)
37
+ @components[component.c_id] = component
38
+ end
39
+
40
+ def infer_component_type(component_id)
41
+ raise UnknownComponent, "Component #{component_id} mising and cannot infer type"
42
+ end
43
+
44
+ def find_component(component_id, build: true)
45
+ component = @components[component_id]
46
+ return component if component
47
+
48
+ return unless build
49
+
50
+ inferred_type = infer_component_type component_id
51
+ component = inferred_type.new node: self, id: component_id
52
+ @components[component_id] = component
53
+ class_name = component.class.name.split('::').last
54
+ class_name << ' component' unless %w[Component ComponentProxy].include?(class_name)
55
+ log "Added component #{component_id} with the inferred type #{class_name}", level: :debug
56
+ component
57
+ end
58
+
59
+ def clear_alarm_timestamps
60
+ @components.each_value(&:clear_alarm_timestamps)
61
+ end
62
+ end
63
+ end