saal 0.2.24 → 0.3.3

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.
@@ -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