wattics-api-client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b1916eb20d15dc10b81e744dc4d0033999ce05d8
4
+ data.tar.gz: 53896a7a281ec057f175096933ef31a1932d372d
5
+ SHA512:
6
+ metadata.gz: 044f756e6c050635ed9655ea7c8e719848366fdb8185fc3274fb5182d50106ca7ace6fcbbc48b2f0cc02d0b079bd0cd9cac392baf754831df2191d6152956ca6
7
+ data.tar.gz: deabf960f636efb6c8b05e7d0a252d5c784ebfeefbe463119f45d0604cc92ee5897e52656ea4efaf90e5a6697b376b95de88f37cace10dfe594edc85b5630eed
@@ -0,0 +1,15 @@
1
+ require 'wattics-api-client/version'
2
+
3
+ require 'wattics-api-client/measurements'
4
+ require 'wattics-api-client/blocking_queue'
5
+ require 'wattics-api-client/config'
6
+ require 'wattics-api-client/client'
7
+ require 'wattics-api-client/measurement_with_config'
8
+
9
+ require 'wattics-api-client/processor'
10
+ require 'wattics-api-client/processor_pool'
11
+
12
+ require 'wattics-api-client/agent'
13
+
14
+ module WatticsApiClient
15
+ end
@@ -0,0 +1,113 @@
1
+ require 'concurrent'
2
+
3
+ class Agent
4
+ @@mutex = Mutex.new
5
+ attr_reader :thread
6
+ def initialize(maximum_parallel_senders = 0)
7
+ @agent_thread_group = ThreadGroup.new
8
+ @processor_pool = ProcessorPool.new(self, @agent_thread_group, maximum_parallel_senders)
9
+ @enqueued_measurements_with_config = Hash.new { |h, k| h[k] = [] }
10
+ @sent_measurements_with_context = BlockingQueue.new
11
+ @measurement_sent_handler_list = Concurrent::Array.new
12
+ start_processor_feeder
13
+ start_measurement_sent_handler_dispatcher
14
+ @wait_semaphore = Concurrent::Semaphore.new(0)
15
+ end
16
+ private_class_method :new
17
+
18
+ def self.get_instance(maximum_parallel_senders = 0)
19
+ @@mutex.synchronize do
20
+ @@instance ||= new(maximum_parallel_senders)
21
+ end
22
+ end
23
+
24
+ def self.dispose
25
+ @@mutex.synchronize do
26
+ unless @@instance.nil?
27
+ @@instance.agent_thread_group.list.each(&:kill)
28
+ @@instance = nil
29
+ end
30
+ end
31
+ end
32
+
33
+ def wait_until_last
34
+ Thread.new do
35
+ sleep 0.01 while @wait_semaphore.available_permits != 0
36
+ end.join
37
+ end
38
+
39
+ def start_processor_feeder
40
+ @agent_thread_group.add(Thread.new do
41
+ begin
42
+ loop do
43
+ key, values = @enqueued_measurements_with_config.first
44
+ if @enqueued_measurements_with_config.empty?
45
+ sleep_fix
46
+ next
47
+ end
48
+ processor = @processor_pool.get_processor(key)
49
+ if processor.nil?
50
+ sleep_fix
51
+ next
52
+ end
53
+ @enqueued_measurements_with_config.delete(key)
54
+ processor.process(values)
55
+ end
56
+ rescue ThreadError
57
+ end
58
+ end)
59
+ end
60
+
61
+ def start_measurement_sent_handler_dispatcher
62
+ @agent_thread_group.add(Thread.new do
63
+ begin
64
+ loop do
65
+ array = @sent_measurements_with_context.pop
66
+ next if array.nil?
67
+ measurement = array[0]
68
+ response = array[1]
69
+ @measurement_sent_handler_list.each { |handler| handler.call(measurement, response) }
70
+ end
71
+ rescue ThreadError
72
+ end
73
+ end)
74
+ end
75
+
76
+ def sleep_fix
77
+ sleep 0.1
78
+ end
79
+
80
+ def send(measurement, config)
81
+ if measurement.is_a?(Array)
82
+ @wait_semaphore.release(measurement.size)
83
+ measurement_groups = measurement.group_by(&:id)
84
+ measurement_groups.each do |channel_id, measurements_for_channel_id|
85
+ measurements_with_config = measurements_for_channel_id.map { |measurement| MeasurementWithConfig.new(measurement, config) }
86
+ @processor_already_bound_to_channel_id = @processor_pool.get_processor(channel_id)
87
+ if @processor_already_bound_to_channel_id.nil?
88
+ @enqueued_measurements_with_config[channel_id] += measurements_with_config
89
+ else
90
+ @processor_already_bound_to_channel_id.process(measurements_with_config)
91
+ end
92
+ end
93
+ else
94
+ @wait_semaphore.release
95
+ measurement_with_config = MeasurementWithConfig.new(measurement, config)
96
+ @processor_already_bound_to_channel_id = @processor_pool.get_processor(measurement.id)
97
+ if @processor_already_bound_to_channel_id.nil?
98
+ @enqueued_measurements_with_config[measurement.id] << measurement_with_config
99
+ else
100
+ @processor_already_bound_to_channel_id.process(measurement_with_config)
101
+ end
102
+ end
103
+ end
104
+
105
+ def report_sent_measurement(measurement, response)
106
+ @sent_measurements_with_context << [measurement, response]
107
+ @wait_semaphore.acquire
108
+ end
109
+
110
+ def add_measurement_sent_handler
111
+ @measurement_sent_handler_list << yield
112
+ end
113
+ end
@@ -0,0 +1,36 @@
1
+ class BlockingQueue
2
+ def initialize
3
+ @mutex = Mutex.new
4
+ @queue = []
5
+ @received = ConditionVariable.new
6
+ end
7
+
8
+ def <<(x)
9
+ @mutex.synchronize do
10
+ @queue << x
11
+ @received.signal
12
+ end
13
+ end
14
+
15
+ def pop
16
+ @mutex.synchronize do
17
+ @received.wait(@mutex) while is_empty?
18
+ @queue.shift
19
+ end
20
+ end
21
+
22
+ def is_empty?
23
+ @queue.empty?
24
+ end
25
+ end
26
+
27
+ class PriorityBlockingQueue < BlockingQueue
28
+ def <<(x)
29
+ @mutex.synchronize do
30
+ @queue << x
31
+ @queue.flatten!
32
+ @queue.sort!
33
+ @received.signal
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,28 @@
1
+ require 'rest-client'
2
+
3
+ class Client
4
+ def send(measurement, config)
5
+ response = RestClient::Request.execute(method: :post, url: config.uri,
6
+ user: config.username, password: config.password,
7
+ payload: measurement.json)
8
+ return response
9
+ rescue RestClient::ExceptionWithResponse => e
10
+ return e.response
11
+ end
12
+ end
13
+
14
+ class ClientFactory
15
+ def self.setInstance(client_factory)
16
+ @instance = client_factory
17
+ end
18
+
19
+ def self.get_instance
20
+ @instance ||= new
21
+ end
22
+
23
+ def create_client
24
+ Client.new
25
+ end
26
+
27
+ private_class_method :new
28
+ end
@@ -0,0 +1,20 @@
1
+ class Config
2
+ attr_reader :environment, :uri, :username, :password
3
+ PRODUCTION = 'https://web-collector.wattics.com/measurements/v2/unifiedjson/'.freeze
4
+ DEVELOPMENT = 'https://dev-web-collector.wattics.com/measurements/v2/unifiedjson/'.freeze
5
+ def initialize(environment, username, password)
6
+ @environment = environment
7
+ @uri = environment(environment)
8
+ @username = username
9
+ @password = password
10
+ end
11
+
12
+ def environment(environment)
13
+ if environment == :PRODUCTION
14
+ PRODUCTION
15
+ elsif environment == :DEVELOPMENT
16
+ DEVELOPMENT
17
+ end
18
+ end
19
+
20
+ end
@@ -0,0 +1,11 @@
1
+ class MeasurementWithConfig
2
+ attr_accessor :measurement, :config
3
+ def initialize(measurement, config)
4
+ @measurement = measurement
5
+ @config = config
6
+ end
7
+
8
+ def <=>(measurementWithConfig)
9
+ @measurement <=> measurementWithConfig.measurement
10
+ end
11
+ end
@@ -0,0 +1,101 @@
1
+ require 'json'
2
+ require 'time'
3
+
4
+ class Measurement
5
+ attr_accessor :id, :timestamp
6
+
7
+ def timestamp=(time)
8
+ @timestamp = time.strftime('%Y-%m-%dT%H:%M:%S.%L%:z')
9
+ end
10
+
11
+ def <=>(measurement)
12
+ @timestamp <=> measurement.timestamp
13
+ end
14
+
15
+ end
16
+
17
+ class SimpleMeasurement < Measurement
18
+ attr_accessor :value
19
+
20
+ def to_s
21
+ 'SimpleMeasurement{' \
22
+ "id='" + @id.to_s + '\'' \
23
+ ', timestamp=' + @timestamp.to_s +
24
+ ', value=' + @value.to_s +
25
+ '}'
26
+ end
27
+
28
+ def json
29
+ {
30
+ id: @id.to_s,
31
+ tsISO8601: @timestamp,
32
+ value: @value
33
+ }.select { |_k, v| v }.to_json
34
+ end
35
+ end
36
+
37
+ class ElectricityMeasurement < Measurement
38
+ attr_accessor :id, :active_power_phase_a, :active_power_phase_b, :active_power_phase_c,
39
+ :reactive_power_phase_a, :reactive_power_phase_b, :reactive_power_phase_c,
40
+ :apparent_power_phase_a, :apparent_power_phase_b, :apparent_power_phase_c,
41
+ :voltage_phase_a, :voltage_phase_b, :voltage_phase_c,
42
+ :current_phase_a, :current_phase_b, :current_phase_c,
43
+ :active_energy_phase_a, :active_energy_phase_b, :active_energy_phase_c,
44
+ :line_to_line_voltage_phase_ab, :line_to_line_voltage_phase_bc, :line_to_line_voltage_phase_ac
45
+
46
+ def to_s
47
+ 'ElectricityMeasurement{' \
48
+ "id='" + @id.to_s + '\'' \
49
+ ', timestamp=' + @timestamp.to_s +
50
+ ', active_power_phase_a=' + @active_power_phase_a.to_s +
51
+ ', active_power_phase_b=' + @active_power_phase_b.to_s +
52
+ ', active_power_phase_c=' + @active_power_phase_c.to_s +
53
+ ', reactive_power_phase_a=' + @reactive_power_phase_a.to_s +
54
+ ', reactive_power_phase_b=' + @reactive_power_phase_b.to_s +
55
+ ', reactive_power_phase_c=' + @reactive_power_phase_c.to_s +
56
+ ', apparent_power_phase_a=' + @apparent_power_phase_a.to_s +
57
+ ', apparent_power_phase_b=' + @apparent_power_phase_b.to_s +
58
+ ', apparent_power_phase_c=' + @apparent_power_phase_c.to_s +
59
+ ', voltage_phase_a=' + @voltage_phase_a.to_s +
60
+ ', voltage_phase_b=' + @voltage_phase_b.to_s +
61
+ ', voltage_phase_c=' + @voltage_phase_c.to_s +
62
+ ', current_phase_a=' + @current_phase_a.to_s +
63
+ ', current_phase_b=' + @current_phase_b.to_s +
64
+ ', current_phase_c=' + @current_phase_c.to_s +
65
+ ', active_energy_phase_a=' + @active_energy_phase_a.to_s +
66
+ ', active_energy_phase_b=' + @active_energy_phase_b.to_s +
67
+ ', active_energy_phase_c=' + @active_energy_phase_c.to_s +
68
+ ', line_to_line_voltage_phase_ab=' + @line_to_line_voltage_phase_ab.to_s +
69
+ ', line_to_line_voltage_phase_ac=' + @line_to_line_voltage_phase_ac.to_s +
70
+ ', line_to_line_voltage_phase_bc=' + @line_to_line_voltage_phase_bc.to_s +
71
+ '}'
72
+ end
73
+
74
+ def json
75
+ {
76
+ id: @id.to_s,
77
+ tsISO8601: @timestamp,
78
+ aP_1: @active_power_phase_a,
79
+ aP_2: @active_power_phase_b,
80
+ aP_3: @active_power_phase_c,
81
+ rP_1: @reactive_power_phase_a,
82
+ rP_2: @reactive_power_phase_b,
83
+ rP_3: @reactive_power_phase_c,
84
+ apP_1: @apparent_power_phase_a,
85
+ apP_2: @apparent_power_phase_b,
86
+ apP_3: @apparent_power_phase_c,
87
+ v_1: @voltage_phase_a,
88
+ v_2: @voltage_phase_b,
89
+ v_3: @voltage_phase_c,
90
+ c_1: @current_phase_a,
91
+ c_2: @current_phase_b,
92
+ c_3: @current_phase_c,
93
+ pC_1: @active_energy_phase_a,
94
+ pC_2: @active_energy_phase_b,
95
+ pC_3: @active_energy_phase_c,
96
+ v_12: @line_to_line_voltage_phase_ab,
97
+ v_13: @line_to_line_voltage_phase_ac,
98
+ v_23: @line_to_line_voltage_phase_bc
99
+ }.select { |_k, v| v }.to_json
100
+ end
101
+ end
@@ -0,0 +1,62 @@
1
+ require 'concurrent'
2
+ require 'nokogiri'
3
+
4
+ class Processor
5
+ def initialize(agent)
6
+ @agent = agent
7
+ @measurements_with_config = PriorityBlockingQueue.new
8
+ @semaphore = Concurrent::Semaphore.new(0)
9
+ @is_sending = false
10
+ @mutex = Mutex.new
11
+ @logger = Logger.new(STDOUT)
12
+ @logger.level = Logger::WARN
13
+ end
14
+
15
+ def process(measurement_with_config)
16
+ @measurements_with_config << measurement_with_config
17
+ if measurement_with_config.is_a?(Array)
18
+ @semaphore.release(measurement_with_config.size)
19
+ else
20
+ @semaphore.release
21
+ end
22
+ end
23
+
24
+ def is_idle?
25
+ @mutex.synchronize do
26
+ @measurements_with_config.is_empty? && !@is_sending
27
+ end
28
+ end
29
+
30
+ def run
31
+ client = ClientFactory.get_instance.create_client
32
+ loop do
33
+ @semaphore.acquire
34
+ @mutex.synchronize do
35
+ @measurement_with_config = @measurements_with_config.pop
36
+ @is_sending = true
37
+ end
38
+ @measurement = @measurement_with_config.measurement
39
+ @config = @measurement_with_config.config
40
+ loop do
41
+ begin
42
+ @response = client.send(@measurement, @config)
43
+ if !@agent.nil? && @response.code < 400
44
+ @agent.report_sent_measurement(@measurement, @response)
45
+ end
46
+ if !@agent.nil? && @response.code >= 400
47
+ @logger.error("Could not send #{@measurement}, Server Response: #{Nokogiri::HTML(@response.body).xpath('//h1').text}")
48
+ end
49
+ break
50
+ rescue StandardError => e
51
+ @logger.error("Could not send #{@measurement}, Server Response: #{e}")
52
+ sleep 60
53
+ end
54
+ end
55
+ @mutex.synchronize do
56
+ @is_sending = false
57
+ end
58
+ end
59
+ rescue StandardError => e
60
+ @logger.error("Thread stopped unexpectedly: #{e.message}")
61
+ end
62
+ end
@@ -0,0 +1,40 @@
1
+ class ProcessorPool
2
+ def initialize(agent, agent_thread_group, maximum_parallel_senders = 0)
3
+ maximum_parallel_senders > 0 ? @max_processors = maximum_parallel_senders.freeze : @max_processors = (2 * Concurrent.processor_count).freeze
4
+ @agent = agent
5
+ @processors = {}
6
+ @processor_thread_group = agent_thread_group
7
+ @mutex = Mutex.new
8
+ end
9
+
10
+ def get_processor(channel_id)
11
+ @mutex.synchronize do
12
+ processor = @processors[channel_id]
13
+ return processor unless processor.nil?
14
+ if @processors.size < @max_processors
15
+ @processors[channel_id] = spawn_new_processor
16
+ @processors[channel_id]
17
+ else
18
+ rebind_processor_to_channel_id(channel_id)
19
+ end
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def spawn_new_processor
26
+ processor = Processor.new(@agent)
27
+ @processor_thread_group.add(Thread.new { processor.run })
28
+ processor
29
+ end
30
+
31
+ def rebind_processor_to_channel_id(new_channel_id)
32
+ idle_processor_entry = @processors.select { |_key, value| value.is_idle? }.first
33
+ return nil if idle_processor_entry.nil?
34
+ old_channel_id = idle_processor_entry[0]
35
+ idle_processor = idle_processor_entry[1]
36
+ @processors.delete(old_channel_id)
37
+ @processors[new_channel_id] = idle_processor
38
+ idle_processor
39
+ end
40
+ end
@@ -0,0 +1,3 @@
1
+ module WatticsApiClient
2
+ VERSION = '0.1.0'.freeze
3
+ end
metadata ADDED
@@ -0,0 +1,181 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: wattics-api-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Wattics
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-02-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry-byebug
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.5'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.5'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rest-client
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: nokogiri
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.6'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.6'
97
+ - !ruby/object:Gem::Dependency
98
+ name: json
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2.1'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2.1'
111
+ - !ruby/object:Gem::Dependency
112
+ name: thread
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.2'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.2'
125
+ - !ruby/object:Gem::Dependency
126
+ name: concurrent-ruby
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '1.0'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '1.0'
139
+ description: The gem connects with the wattics API to send over data to the wattics
140
+ plataform
141
+ email:
142
+ - support@wattics.com
143
+ executables: []
144
+ extensions: []
145
+ extra_rdoc_files: []
146
+ files:
147
+ - lib/wattics-api-client.rb
148
+ - lib/wattics-api-client/agent.rb
149
+ - lib/wattics-api-client/blocking_queue.rb
150
+ - lib/wattics-api-client/client.rb
151
+ - lib/wattics-api-client/config.rb
152
+ - lib/wattics-api-client/measurement_with_config.rb
153
+ - lib/wattics-api-client/measurements.rb
154
+ - lib/wattics-api-client/processor.rb
155
+ - lib/wattics-api-client/processor_pool.rb
156
+ - lib/wattics-api-client/version.rb
157
+ homepage: https://github.com/Wattics/wattics-api-client
158
+ licenses:
159
+ - MIT
160
+ metadata: {}
161
+ post_install_message:
162
+ rdoc_options: []
163
+ require_paths:
164
+ - lib
165
+ required_ruby_version: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - ">="
168
+ - !ruby/object:Gem::Version
169
+ version: '0'
170
+ required_rubygems_version: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - ">="
173
+ - !ruby/object:Gem::Version
174
+ version: '0'
175
+ requirements: []
176
+ rubyforge_project:
177
+ rubygems_version: 2.6.14
178
+ signing_key:
179
+ specification_version: 4
180
+ summary: This gem connects with Wattics API
181
+ test_files: []