saal 0.2.24 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -42,6 +42,7 @@ module SAAL
42
42
  @cache_timeout = opts[:cache_timeout] || opts['cache_timeout'] || DEFAULT_CACHE_TIMEOUT
43
43
  @cache = nil
44
44
  @cachehit = nil
45
+ @cachetime = nil
45
46
  end
46
47
 
47
48
  def state(num)
@@ -60,25 +61,8 @@ module SAAL
60
61
 
61
62
  private
62
63
  def do_get(path)
63
- begin
64
- http = Net::HTTP.new(@host,@port)
65
- # Timeout faster when the other side doesn't respond
66
- http.open_timeout = @timeout
67
- http.read_timeout = @timeout
68
- req = Net::HTTP::Get.new(path)
69
- req.basic_auth @user, @pass
70
- response = http.request(req)
71
- if response.code != "200"
72
- #$stderr.puts "ERROR: Code #{response.code}"
73
- #$stderr.puts response.body
74
- return nil
75
- end
76
- return response
77
- rescue Exception
78
- return nil
79
- end
64
+ SAAL::do_http_get(@host, @port, path, @user, @pass, @timeout)
80
65
  end
81
-
82
66
  def parse_index_html(str)
83
67
  doc = Nokogiri::HTML(str)
84
68
  outlets = doc.css('tr[bgcolor="#F4F4F4"]')
@@ -0,0 +1,287 @@
1
+ require 'json'
2
+
3
+ module SAAL
4
+ module Envoy
5
+ class PowerEnergyUnderlying < SensorUnderlying
6
+ def initialize(key, production)
7
+ @key = key
8
+ @production = production
9
+ end
10
+
11
+ def read(uncached = false)
12
+ @production.read_val(@key)
13
+ end
14
+ end
15
+
16
+ class PowerEnergy
17
+ DEFAULT_HOST = "envoy.local"
18
+ DEFAULT_TIMEOUT = 2
19
+ DEFAULT_CACHE_TIMEOUT = 50
20
+ DEFAULT_SOURCES = [
21
+ "production_inverters",
22
+ "production_phase1", "production_phase2", "production_phase3", "production_total",
23
+ "net_consumption_phase1", "net_consumption_phase2", "net_consumption_phase3", "net_consumption_total",
24
+ "total_consumption_phase1", "total_consumption_phase2", "total_consumption_phase3", "total_consumption_total",
25
+ ]
26
+ DEFAULT_TYPES = [
27
+ "w_now", "wh_lifetime", "va_now", "vah_lifetime",
28
+ ]
29
+ DEFAULT_PREFIX = "pv"
30
+
31
+ def initialize(defs, opts={})
32
+ @host = defs[:host] || defs['host'] || DEFAULT_HOST
33
+ @timeout = opts[:timeout] || opts['timeout'] || DEFAULT_TIMEOUT
34
+ @cache_timeout = opts[:cache_timeout] || opts['cache_timeout'] || DEFAULT_CACHE_TIMEOUT
35
+ @cache = nil
36
+ @cachetime = nil
37
+ @sources = defs[:sources] || defs['source'] || DEFAULT_SOURCES
38
+ @types = defs[:types] || defs['types'] || DEFAULT_TYPES
39
+ @prefix = defs[:prefix] || defs['prefix'] || DEFAULT_PREFIX
40
+ end
41
+
42
+ def read_val(name)
43
+ if !@cachetime or @cachetime < Time.now - @cache_timeout
44
+ @cache = read_all()
45
+ @cachetime = Time.now
46
+ end
47
+ return @cache ? @cache[name] : nil
48
+ end
49
+
50
+ def create_sensors
51
+ sensors = {}
52
+ @sources.product(@types).each do |source, type|
53
+ key = "#{@prefix}_#{source}_#{type}"
54
+ sensors[key] = PowerEnergyUnderlying.new(key, self)
55
+ end
56
+ sensors
57
+ end
58
+
59
+ private
60
+ def save_vals(dest, name, source)
61
+ {
62
+ "wNow" => "w_now",
63
+ "apprntPwr" => "va_now",
64
+ "whLifetime" => "wh_lifetime",
65
+ "vahLifetime" => "vah_lifetime",
66
+ }.each do |type, label|
67
+ dest["#{@prefix}_#{name}_#{label}"] = source[type]
68
+ end
69
+
70
+ # Hack around the fact that apprntPwr is broken on the total consumption
71
+ # calculation for the three-phase sum at least
72
+ # In those cases it seems to be missing a divide by three, so when the
73
+ # calculation for voltage and current alone is close do the extra divide
74
+ va_now = dest["#{@prefix}_#{name}_va_now"]
75
+ if va_now && !name.include?("phase")
76
+ voltage = source["rmsVoltage"]
77
+ current = source["rmsCurrent"]
78
+ if voltage && current
79
+ va_alt = voltage * current
80
+ if ((va_alt / va_now) - 1.0).abs < 0.05
81
+ dest["#{@prefix}_#{name}_va_now"] = va_now / 3.0
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ def read_all
88
+ response = SAAL::do_http_get(@host, 80, "/production.json?details=1", nil, nil, @timeout)
89
+ return nil if !response
90
+
91
+ values = JSON.parse(response.body)
92
+ outputs = {}
93
+
94
+ values["production"].each do |source|
95
+ type = source["type"]
96
+ case type
97
+ when "inverters"
98
+ save_vals(outputs, "production_inverters", source)
99
+ when "eim"
100
+ if source["lines"]
101
+ save_vals(outputs, "production_phase1", source["lines"][0])
102
+ save_vals(outputs, "production_phase2", source["lines"][1])
103
+ save_vals(outputs, "production_phase3", source["lines"][2])
104
+ end
105
+ save_vals(outputs, "production_total", source)
106
+ else
107
+ $stderr.puts "WARNING: ENVOY: don't know source type #{type}"
108
+ end
109
+ end
110
+
111
+ values["consumption"].each do |source|
112
+ type = {
113
+ "total-consumption" => "total",
114
+ "net-consumption" => "net",
115
+ }[source["measurementType"]] || "unknown";
116
+
117
+ if source["lines"]
118
+ save_vals(outputs, "#{type}_consumption_phase1", source["lines"][0])
119
+ save_vals(outputs, "#{type}_consumption_phase2", source["lines"][1])
120
+ save_vals(outputs, "#{type}_consumption_phase3", source["lines"][2])
121
+ end
122
+ save_vals(outputs, "#{type}_consumption_total", source)
123
+ end
124
+
125
+ outputs
126
+ end
127
+ end
128
+
129
+ class ACQualityUnderlying < SensorUnderlying
130
+ def initialize(key, production)
131
+ @key = key
132
+ @production = production
133
+ end
134
+
135
+ def read(uncached = false)
136
+ @production.read_val(@key)
137
+ end
138
+ end
139
+
140
+ class ACQuality
141
+ DEFAULT_HOST = "envoy.local"
142
+ DEFAULT_TIMEOUT = 2
143
+ DEFAULT_CACHE_TIMEOUT = 50
144
+ DEFAULT_SOURCES = ["total","phase1","phase2","phase3",]
145
+ DEFAULT_TYPES = ["frequency","voltage"]
146
+ DEFAULT_PREFIX = "ac"
147
+
148
+ def initialize(defs, opts={})
149
+ @host = defs[:host] || defs['host'] || DEFAULT_HOST
150
+ @timeout = opts[:timeout] || opts['timeout'] || DEFAULT_TIMEOUT
151
+ @cache_timeout = opts[:cache_timeout] || opts['cache_timeout'] || DEFAULT_CACHE_TIMEOUT
152
+ @cache = nil
153
+ @cachetime = nil
154
+ @sources = defs[:sources] || defs['source'] || DEFAULT_SOURCES
155
+ @types = defs[:types] || defs['types'] || DEFAULT_TYPES
156
+ @prefix = defs[:prefix] || defs['prefix'] || DEFAULT_PREFIX
157
+ end
158
+
159
+ def read_val(name)
160
+ if !@cachetime or @cachetime < Time.now - @cache_timeout
161
+ @cache = read_all()
162
+ @cachetime = Time.now
163
+ end
164
+ return @cache ? @cache[name] : nil
165
+ end
166
+
167
+ def create_sensors
168
+ sensors = {}
169
+ @sources.product(@types).each do |source, type|
170
+ key = "#{@prefix}_#{source}_#{type}"
171
+ sensors[key] = ACQualityUnderlying.new(key, self)
172
+ end
173
+ sensors
174
+ end
175
+
176
+ private
177
+ def save_vals(dest, name, source)
178
+ {
179
+ "voltage" => "voltage",
180
+ "freq" => "frequency",
181
+ }.each do |type, label|
182
+ dest["#{@prefix}_#{name}_#{label}"] = source[type]
183
+ end
184
+ end
185
+
186
+ def read_all
187
+ response = SAAL::do_http_get(@host, 80, "/ivp/meters/readings", nil, nil, @timeout)
188
+ return nil if !response
189
+
190
+ values = JSON.parse(response.body)
191
+ outputs = {}
192
+ source = values[0]
193
+ save_vals(outputs, "total", source)
194
+ if source["channels"]
195
+ save_vals(outputs, "phase1", source["channels"][0])
196
+ save_vals(outputs, "phase2", source["channels"][1])
197
+ save_vals(outputs, "phase3", source["channels"][2])
198
+ end
199
+
200
+ outputs
201
+ end
202
+ end
203
+
204
+ class InverterUnderlying < SensorUnderlying
205
+ def initialize(key, inverters)
206
+ @key = key
207
+ @inverters = inverters
208
+ end
209
+
210
+ def read(uncached = false)
211
+ @inverters.read_val(@key)
212
+ end
213
+ end
214
+
215
+ class Inverters
216
+ DEFAULT_TIMEOUT = 2
217
+ DEFAULT_CACHE_TIMEOUT = 50
218
+ DEFAULT_SOURCES = []
219
+ DEFAULT_TYPES = ["w_now"] # "last_report_date", "w_max"
220
+ DEFAULT_USER = nil
221
+ DEFAULT_PASSWORD = nil
222
+ DEFAULT_PREFIX = "inverters"
223
+ attr_reader :inverters
224
+
225
+ def initialize(defs, opts={})
226
+ @host = defs[:host] || defs['host'] || DEFAULT_HOST
227
+ @user = defs[:user] || defs['user'] || DEFAULT_USER
228
+ @password = defs[:password] || defs['password'] || DEFAULT_PASSWORD
229
+ @timeout = opts[:timeout] || opts['timeout'] || DEFAULT_TIMEOUT
230
+ @cache_timeout = opts[:cache_timeout] || opts['cache_timeout'] || DEFAULT_CACHE_TIMEOUT
231
+ @cache = nil
232
+ @cachetime = nil
233
+ @inverters_list = {}
234
+ @inverters = defs[:inverters] || defs['inverters'] || DEFAULT_SOURCES
235
+ @types = defs[:types] || defs['types'] || DEFAULT_TYPES
236
+ @prefix = defs[:prefix] || defs['prefix'] || DEFAULT_PREFIX
237
+ end
238
+
239
+ def read_val(name)
240
+ if !@cachetime or @cachetime < Time.now - @cache_timeout
241
+ @cache = read_all()
242
+ @cachetime = Time.now
243
+ end
244
+ return @cache ? @cache[name] : nil
245
+ end
246
+
247
+ def enumerate
248
+ read_val("foo") # Force a read to make sure the inverter serials are stored
249
+ @inverters_list.keys
250
+ end
251
+
252
+ def set_all_inverters!
253
+ @inverters = self.enumerate
254
+ end
255
+
256
+ def create_sensors
257
+ sensors = {}
258
+ @inverters.product(@types).each do |source, type|
259
+ key = "#{@prefix}_#{source}_#{type}"
260
+ sensors[key] = InverterUnderlying.new(key, self)
261
+ end
262
+ sensors
263
+ end
264
+
265
+ private
266
+ def read_all
267
+ response = SAAL::do_http_get_digest(@host, 80, "/api/v1/production/inverters", @user, @password, @timeout)
268
+ return nil if !response
269
+
270
+ values = JSON.parse(response.body)
271
+ inverters = {}
272
+ values.each do |inverter|
273
+ serial = inverter["serialNumber"]
274
+ @inverters_list[serial] = true
275
+ {"lastReportDate" => "last_report_date",
276
+ "lastReportWatts" => "w_now",
277
+ "maxReportWatts" => "w_max",
278
+ }.each do |type, label|
279
+ inverters["#{@prefix}_#{serial}_#{label}"] = inverter[type]
280
+ end
281
+ end
282
+
283
+ inverters
284
+ end
285
+ end
286
+ end
287
+ end
@@ -0,0 +1,51 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'net/http/digest_auth'
4
+
5
+ def SAAL::do_http_get(host, port, path, user, pass, timeout)
6
+ begin
7
+ http = Net::HTTP.new(host,port)
8
+ # Timeout faster when the other side doesn't respond
9
+ http.open_timeout = timeout
10
+ http.read_timeout = timeout
11
+ req = Net::HTTP::Get.new(path)
12
+ req.basic_auth(user, pass) if user && pass
13
+ response = http.request(req)
14
+ if response.code != "200"
15
+ #$stderr.puts "ERROR: Code #{response.code}"
16
+ #$stderr.puts response.body
17
+ return nil
18
+ end
19
+ return response
20
+ rescue Exception
21
+ return nil
22
+ end
23
+ end
24
+
25
+ def SAAL::do_http_get_digest(host, port, path, user, pass, timeout)
26
+ begin
27
+ uri = URI.parse "http://#{host}:#{port}/#{path}"
28
+ digest_auth = Net::HTTP::DigestAuth.new
29
+ uri.user = user
30
+ uri.password = pass
31
+ http = Net::HTTP.new(host,port)
32
+ # Timeout faster when the other side doesn't respond
33
+ http.open_timeout = timeout
34
+ http.read_timeout = timeout
35
+ req = Net::HTTP::Get.new(path)
36
+ response = http.request(req)
37
+ if response.code == "401" && user && pass
38
+ auth = digest_auth.auth_header uri, response['www-authenticate'], 'GET'
39
+ req.add_field 'Authorization', auth
40
+ response = http.request(req)
41
+ end
42
+ if response.code != "200"
43
+ #$stderr.puts "ERROR: Code #{response.code}"
44
+ #$stderr.puts response.body
45
+ return nil
46
+ end
47
+ return response
48
+ rescue Exception
49
+ return nil
50
+ end
51
+ end
@@ -41,7 +41,7 @@ module SAAL
41
41
  def valid_cache
42
42
  @compcache.sort!
43
43
  central = @compcache[1..(@compcache.size-2)]
44
- sum = central.inject(0.0){|sum,el| sum+el}
44
+ sum = central.inject(0.0){|csum,el| csum+el}
45
45
  return false if sum == 0.0
46
46
  average = sum/central.size
47
47
  central.each do |el|
@@ -1,5 +1,5 @@
1
1
  require 'yaml'
2
- require "mysql"
2
+ require "mysql2"
3
3
  require 'ownet'
4
4
  require 'nokogiri'
5
5
  require 'erb'
@@ -10,7 +10,7 @@ module SAAL
10
10
  DBCONF = CONFDIR+"database.yml"
11
11
  CHARTSCONF = CONFDIR+"charts.yml"
12
12
 
13
- VERSION = '0.2.24'
13
+ VERSION = '0.3.3'
14
14
  end
15
15
 
16
16
  require File.dirname(__FILE__)+'/dbstore.rb'
@@ -23,4 +23,7 @@ require File.dirname(__FILE__)+'/chart.rb'
23
23
  require File.dirname(__FILE__)+'/chart_data.rb'
24
24
  require File.dirname(__FILE__)+'/outliercache.rb'
25
25
  require File.dirname(__FILE__)+'/dinrelay.rb'
26
+ require File.dirname(__FILE__)+'/envoy.rb'
27
+ require File.dirname(__FILE__)+'/http.rb'
28
+ require File.dirname(__FILE__)+'/denkovi.rb'
26
29
 
@@ -40,9 +40,38 @@ module SAAL
40
40
  defs.merge!('name' => outlet_descriptions[num])
41
41
  Sensor.new(dbstore, oname, DINRelay::Outlet.new(num.to_i, og), defs, opts)
42
42
  end
43
+ elsif defs['envoy_power_energy']
44
+ defs = defs['envoy_power_energy'].merge('prefix' => name)
45
+ pe = SAAL::Envoy::PowerEnergy::new(defs)
46
+ sensors = pe.create_sensors
47
+ return sensors.map do |name, underlying|
48
+ Sensor.new(dbstore, name, underlying, defs, opts)
49
+ end
50
+ elsif defs['envoy_ac_quality']
51
+ defs = defs['envoy_ac_quality'].merge('prefix' => name)
52
+ pe = SAAL::Envoy::ACQuality::new(defs)
53
+ sensors = pe.create_sensors
54
+ return sensors.map do |name, underlying|
55
+ Sensor.new(dbstore, name, underlying, defs, opts)
56
+ end
57
+ elsif defs['envoy_inverters']
58
+ defs = defs['envoy_inverters'].merge('prefix' => name)
59
+ pe = SAAL::Envoy::Inverters::new(defs)
60
+ sensors = pe.create_sensors
61
+ return sensors.map do |name, underlying|
62
+ Sensor.new(dbstore, name, underlying, defs, opts)
63
+ end
64
+ elsif defs['denkovi']
65
+ defs = defs['denkovi'].merge('prefix' => name)
66
+ denkovi = SAAL::Denkovi::OutletGroup::new(defs)
67
+ sensors = denkovi.create_sensors
68
+ return sensors.map do |name, vals|
69
+ underlying, description = vals
70
+ defs.merge!('name' => description)
71
+ Sensor.new(dbstore, name, underlying, defs, opts)
72
+ end
43
73
  else
44
- raise UnknownSensorType, "Couldn't figure out a valid sensor type "
45
- "from the configuration for #{name}"
74
+ $stderror.puts "WARNING: Couldn't figure out a valid sensor type for #{name}"
46
75
  end
47
76
  end
48
77
  end
@@ -6,8 +6,8 @@ Gem::Specification.new do |s|
6
6
  s.platform = Gem::Platform::RUBY
7
7
 
8
8
  s.name = 'saal'
9
- s.version = '0.2.24'
10
- s.date = '2013-11-23'
9
+ s.version = '0.3.3'
10
+ s.date = '2020-12-28'
11
11
 
12
12
  s.summary = "Thin abstraction layer for interfacing and recording sensors (currently onewire) and actuators (currently dinrelay)"
13
13
  s.description = <<EOF
@@ -19,6 +19,7 @@ EOF
19
19
  s.authors = ["Pedro Côrte-Real"]
20
20
  s.email = 'pedro@pedrocr.net'
21
21
  s.homepage = 'https://github.com/pedrocr/saal'
22
+ s.licenses = 'LGPL-2.1'
22
23
 
23
24
  s.require_paths = %w[lib]
24
25
 
@@ -28,9 +29,10 @@ EOF
28
29
 
29
30
  s.executables = Dir.glob("bin/*").map{|f| f.gsub('bin/','')}
30
31
 
31
- s.add_dependency('ownet', [">= 0.2.1"])
32
- s.add_dependency('nokogiri')
33
- s.add_dependency('mysql')
32
+ s.add_runtime_dependency 'ownet', "~>0.2"
33
+ s.add_runtime_dependency 'mysql2', "~>0.5"
34
+ s.add_runtime_dependency 'nokogiri', '~>1.8'
35
+ s.add_runtime_dependency 'net-http-digest_auth', '~>1.4'
34
36
 
35
37
  # = MANIFEST =
36
38
  s.files = %w[
@@ -43,7 +45,10 @@ EOF
43
45
  bin/dinrelaystatus
44
46
  bin/saal_chart
45
47
  bin/saal_daemon
48
+ bin/saal_denkovi_relays
46
49
  bin/saal_dump_database
50
+ bin/saal_envoy_generate_config
51
+ bin/saal_envoy_read
47
52
  bin/saal_import_mysql
48
53
  bin/saal_readall
49
54
  lib/chart.rb
@@ -51,7 +56,10 @@ EOF
51
56
  lib/charts.rb
52
57
  lib/daemon.rb
53
58
  lib/dbstore.rb
59
+ lib/denkovi.rb
54
60
  lib/dinrelay.rb
61
+ lib/envoy.rb
62
+ lib/http.rb
55
63
  lib/outliercache.rb
56
64
  lib/owsensor.rb
57
65
  lib/saal.rb
@@ -63,6 +71,8 @@ EOF
63
71
  test/charts_test.rb
64
72
  test/daemon_test.rb
65
73
  test/dbstore_test.rb
74
+ test/denkovi.json.erb
75
+ test/denkovi_test.rb
66
76
  test/dinrelay.html.erb
67
77
  test/dinrelay_test.rb
68
78
  test/nonexistant_sensor.yml
@@ -71,6 +81,7 @@ EOF
71
81
  test/sensors_test.rb
72
82
  test/test_charts.yml
73
83
  test/test_db.yml
84
+ test/test_denkovi_sensors.yml
74
85
  test/test_dinrelay_sensors.yml
75
86
  test/test_helper.rb
76
87
  test/test_sensor_cleanups.yml