wattics-api-client 0.1.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.
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: []