saal 0.3.0 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/saal_envoy_generate_config +53 -0
- data/bin/saal_envoy_read +143 -0
- data/lib/dinrelay.rb +1 -18
- data/lib/envoy.rb +284 -0
- data/lib/http.rb +51 -0
- data/lib/saal.rb +3 -1
- data/lib/sensors.rb +16 -2
- data/saal.gemspec +6 -2
- metadata +26 -11
- data/bin/saal_chart~ +0 -90
- data/bin/saal_daemon~ +0 -31
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 62d7f9b51083356448d50bcb5ad66c729d84a38e152f85470ca4fa5f9f5fed15
|
4
|
+
data.tar.gz: 73f51e4736d14ee249531535577a8ff0568e0c19fb3115a4cf76bb0801d8e504
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 349eed054b1fd37de656840840b92e1be279e77fe677b92cc47864fe96079f88e6dc784e608bf5a7843b08cc0025b1f141d1bf88be1cef553e18f104cc0c3d74
|
7
|
+
data.tar.gz: e07a22761c8f4f6c01d631abb29b01c63d6888c449fd67762e558ddd7693cd1bdfdf5b8bd7cdc35575006c6cb2fc9cbef49329bf08b754d5101d5dfc20d3421d
|
@@ -0,0 +1,53 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.dirname(__FILE__)+'/../lib/saal.rb'
|
4
|
+
|
5
|
+
if ARGV.size != 1 && ARGV.size != 3
|
6
|
+
$stderr.puts "USAGE: saal_envoy_generate_config <host> [<user> <password>]"
|
7
|
+
exit(1)
|
8
|
+
end
|
9
|
+
|
10
|
+
puts "power:"
|
11
|
+
puts " envoy_power_energy:"
|
12
|
+
puts " host: \"#{ARGV[0]}\""
|
13
|
+
puts
|
14
|
+
puts "ac:"
|
15
|
+
puts " envoy_ac_quality:"
|
16
|
+
puts " host: \"#{ARGV[0]}\""
|
17
|
+
|
18
|
+
user = "envoy"
|
19
|
+
password = nil
|
20
|
+
|
21
|
+
if ARGV[2]
|
22
|
+
user = ARGV[1]
|
23
|
+
password = ARGV[2]
|
24
|
+
else
|
25
|
+
Net::HTTP.get(ARGV[0], '/home').split("\n").each do |line|
|
26
|
+
if line.include?("serial:")
|
27
|
+
password = line.split('"')[1][-6..-1]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
if !password
|
31
|
+
$stderr.puts "Couldn't find serial number for envoy"
|
32
|
+
exit 2
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
envoy = SAAL::Envoy::Inverters::new(
|
37
|
+
:host => ARGV[0],
|
38
|
+
:user => user,
|
39
|
+
:password => password,
|
40
|
+
)
|
41
|
+
envoy.set_all_inverters!
|
42
|
+
exit 0 if envoy.inverters.size <= 0
|
43
|
+
|
44
|
+
puts
|
45
|
+
puts "inverters:"
|
46
|
+
puts " envoy_inverters:"
|
47
|
+
puts " host: \"#{ARGV[0]}\""
|
48
|
+
puts " user: \"#{user}\""
|
49
|
+
puts " password: \"#{password}\""
|
50
|
+
puts " inverters:"
|
51
|
+
envoy.inverters.each do |serial|
|
52
|
+
puts " - #{serial}"
|
53
|
+
end
|
data/bin/saal_envoy_read
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.dirname(__FILE__)+'/../lib/saal.rb'
|
4
|
+
|
5
|
+
if ARGV.size != 3
|
6
|
+
$stderr.puts "USAGE: saal_envoy_read <host> <user> <password>"
|
7
|
+
exit(1)
|
8
|
+
end
|
9
|
+
|
10
|
+
def fdisp(val)
|
11
|
+
if val
|
12
|
+
'%10.0f' % val
|
13
|
+
else
|
14
|
+
" n/a"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def fdispk(val)
|
19
|
+
if val
|
20
|
+
'%10.2f' % (val / 1000.0)
|
21
|
+
else
|
22
|
+
" n/a"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def fdisp_dec(val)
|
27
|
+
if val
|
28
|
+
'%10.2f' % val
|
29
|
+
else
|
30
|
+
" n/a"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def l(vals, name)
|
35
|
+
sensor = vals[name]
|
36
|
+
if sensor
|
37
|
+
sensor.read
|
38
|
+
else
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def lratio(vals, name1, name2)
|
44
|
+
v1 = l(vals, name1)
|
45
|
+
v2 = l(vals, name2)
|
46
|
+
if v1 && v2
|
47
|
+
v1 / v2
|
48
|
+
else
|
49
|
+
nil
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
ac_quality = SAAL::Envoy::ACQuality::new(:host => ARGV[0]).create_sensors
|
54
|
+
|
55
|
+
puts " ========= AC QUALITY ========"
|
56
|
+
puts " voltage (V) freq (Hz)"
|
57
|
+
def qual_line(vals,name, type)
|
58
|
+
puts " #{name} \
|
59
|
+
#{fdisp_dec(l(vals,"ac_#{type}_voltage"))} \
|
60
|
+
#{fdisp_dec(l(vals,"ac_#{type}_frequency"))} \
|
61
|
+
"
|
62
|
+
end
|
63
|
+
qual_line(ac_quality, "Total: ", "total")
|
64
|
+
qual_line(ac_quality, "Phase1:", "phase1")
|
65
|
+
qual_line(ac_quality, "Phase2:", "phase2")
|
66
|
+
qual_line(ac_quality, "Phase3:", "phase3")
|
67
|
+
|
68
|
+
production = SAAL::Envoy::PowerEnergy::new(:host => ARGV[0]).create_sensors
|
69
|
+
|
70
|
+
puts ""
|
71
|
+
puts " ============ TRUE POWER (W) ============ ======= TRUE ENERGY (kWh) ======="
|
72
|
+
puts " consumption production net consumption production net"
|
73
|
+
def p_line(vals, name, type, metric)
|
74
|
+
puts " #{name} \
|
75
|
+
#{fdisp(l(vals,"pv_total_consumption_#{type}_#{metric}_now"))} \
|
76
|
+
#{fdisp(l(vals,"pv_production_#{type}_#{metric}_now"))} \
|
77
|
+
#{fdisp(l(vals,"pv_net_consumption_#{type}_#{metric}_now"))} \
|
78
|
+
#{fdispk(l(vals,"pv_total_consumption_#{type}_#{metric}h_lifetime"))} \
|
79
|
+
#{fdispk(l(vals,"pv_production_#{type}_#{metric}h_lifetime"))} \
|
80
|
+
#{fdispk(l(vals,"pv_net_consumption_#{type}_#{metric}h_lifetime"))} \
|
81
|
+
"
|
82
|
+
end
|
83
|
+
p_line(production, "Total: ", "total", "w")
|
84
|
+
p_line(production, "Phase1:", "phase1", "w")
|
85
|
+
p_line(production, "Phase2:", "phase2", "w")
|
86
|
+
p_line(production, "Phase3:", "phase3", "w")
|
87
|
+
puts " Total Inverters: \
|
88
|
+
#{fdisp(l(production,"pv_production_inverters_w_now"))} \
|
89
|
+
\
|
90
|
+
#{fdispk(l(production,"pv_production_inverters_wh_lifetime"))} \
|
91
|
+
"
|
92
|
+
|
93
|
+
puts ""
|
94
|
+
puts " ========== APPARENT POWER (VA) ========= ===== APPARENT ENERGY (kVAh) ===="
|
95
|
+
puts " consumption production net consumption production net"
|
96
|
+
p_line(production, "Total: ", "total", "va")
|
97
|
+
p_line(production, "Phase1:", "phase1", "va")
|
98
|
+
p_line(production, "Phase2:", "phase2", "va")
|
99
|
+
p_line(production, "Phase3:", "phase3", "va")
|
100
|
+
|
101
|
+
def pf_line(vals, name, type, metric)
|
102
|
+
pf_total_consumption_instant = lratio(vals,"pv_total_consumption_#{type}_w_now","pv_total_consumption_#{type}_va_now")
|
103
|
+
pf_total_production_instant = lratio(vals,"pv_production_#{type}_w_now","pv_production_#{type}_va_now")
|
104
|
+
pf_net_production_instant = lratio(vals,"pv_net_consumption_#{type}_w_now","pv_net_consumption_#{type}_va_now")
|
105
|
+
|
106
|
+
pf_total_consumption_lifetime = lratio(vals,"pv_total_consumption_#{type}_wh_lifetime","pv_total_consumption_#{type}_vah_lifetime")
|
107
|
+
pf_total_production_lifetime = lratio(vals,"pv_production_#{type}_wh_lifetime","pv_production_#{type}_vah_lifetime")
|
108
|
+
pf_net_production_lifetime = lratio(vals,"pv_net_consumption_#{type}_wh_lifetime","pv_net_consumption_#{type}_vah_lifetime")
|
109
|
+
|
110
|
+
puts " #{name} \
|
111
|
+
#{fdisp_dec(pf_total_consumption_instant)} \
|
112
|
+
#{fdisp_dec(pf_total_production_instant)} \
|
113
|
+
#{fdisp_dec(pf_net_production_instant)} \
|
114
|
+
#{fdisp_dec(pf_total_consumption_lifetime)} \
|
115
|
+
#{fdisp_dec(pf_total_production_lifetime)} \
|
116
|
+
#{fdisp_dec(pf_net_production_lifetime)} \
|
117
|
+
"
|
118
|
+
end
|
119
|
+
|
120
|
+
puts ""
|
121
|
+
puts " ========= INSTANT POWER FACTOR ========= ====== LIFETIME POWER FACTOR ====="
|
122
|
+
puts " consumption production net consumption production net"
|
123
|
+
pf_line(production, "Total: ", "total", "va")
|
124
|
+
pf_line(production, "Phase1:", "phase1", "va")
|
125
|
+
pf_line(production, "Phase2:", "phase2", "va")
|
126
|
+
pf_line(production, "Phase3:", "phase3", "va")
|
127
|
+
|
128
|
+
puts ""
|
129
|
+
envoy = SAAL::Envoy::Inverters::new(
|
130
|
+
:host => ARGV[0],
|
131
|
+
:user => ARGV[1],
|
132
|
+
:password => ARGV[2],
|
133
|
+
)
|
134
|
+
envoy.set_all_inverters!
|
135
|
+
inverters = envoy.create_sensors
|
136
|
+
puts "Found #{envoy.inverters.size} inverters"
|
137
|
+
envoy.inverters.each do |serial|
|
138
|
+
puts "INVERTER: #{serial} \
|
139
|
+
date:#{l(inverters,"inverter_#{serial}_last_report_date")} \
|
140
|
+
lastWatts:#{l(inverters,"inverter_#{serial}_w_now")} \
|
141
|
+
maxWatts:#{l(inverters,"inverter_#{serial}_w_max")} \
|
142
|
+
"
|
143
|
+
end
|
data/lib/dinrelay.rb
CHANGED
@@ -61,25 +61,8 @@ module SAAL
|
|
61
61
|
|
62
62
|
private
|
63
63
|
def do_get(path)
|
64
|
-
|
65
|
-
http = Net::HTTP.new(@host,@port)
|
66
|
-
# Timeout faster when the other side doesn't respond
|
67
|
-
http.open_timeout = @timeout
|
68
|
-
http.read_timeout = @timeout
|
69
|
-
req = Net::HTTP::Get.new(path)
|
70
|
-
req.basic_auth @user, @pass
|
71
|
-
response = http.request(req)
|
72
|
-
if response.code != "200"
|
73
|
-
#$stderr.puts "ERROR: Code #{response.code}"
|
74
|
-
#$stderr.puts response.body
|
75
|
-
return nil
|
76
|
-
end
|
77
|
-
return response
|
78
|
-
rescue Exception
|
79
|
-
return nil
|
80
|
-
end
|
64
|
+
SAAL::do_http_get(@host, @port, path, @user, @pass, @timeout)
|
81
65
|
end
|
82
|
-
|
83
66
|
def parse_index_html(str)
|
84
67
|
doc = Nokogiri::HTML(str)
|
85
68
|
outlets = doc.css('tr[bgcolor="#F4F4F4"]')
|
data/lib/envoy.rb
ADDED
@@ -0,0 +1,284 @@
|
|
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 = ["last_report_date", "watts_now", "watts_max"]
|
220
|
+
DEFAULT_USER = nil
|
221
|
+
DEFAULT_PASSWORD = nil
|
222
|
+
attr_reader :inverters
|
223
|
+
|
224
|
+
def initialize(defs, opts={})
|
225
|
+
@host = defs[:host] || defs['host'] || DEFAULT_HOST
|
226
|
+
@user = defs[:user] || defs['user'] || DEFAULT_USER
|
227
|
+
@password = defs[:password] || defs['password'] || DEFAULT_PASSWORD
|
228
|
+
@timeout = opts[:timeout] || opts['timeout'] || DEFAULT_TIMEOUT
|
229
|
+
@cache_timeout = opts[:cache_timeout] || opts['cache_timeout'] || DEFAULT_CACHE_TIMEOUT
|
230
|
+
@cache = nil
|
231
|
+
@cachetime = nil
|
232
|
+
@inverters_list = {}
|
233
|
+
@inverters = defs[:inverters] || defs['inverters'] || DEFAULT_SOURCES
|
234
|
+
@types = defs[:types] || defs['types'] || DEFAULT_TYPES
|
235
|
+
end
|
236
|
+
|
237
|
+
def read_val(name)
|
238
|
+
if !@cachetime or @cachetime < Time.now - @cache_timeout
|
239
|
+
@cache = read_all()
|
240
|
+
@cachetime = Time.now
|
241
|
+
end
|
242
|
+
return @cache ? @cache[name] : nil
|
243
|
+
end
|
244
|
+
|
245
|
+
def enumerate
|
246
|
+
read_val("foo") # Force a read to make sure the inverter serials are stored
|
247
|
+
@inverters_list.keys
|
248
|
+
end
|
249
|
+
|
250
|
+
def set_all_inverters!
|
251
|
+
@inverters = self.enumerate
|
252
|
+
end
|
253
|
+
|
254
|
+
def create_sensors
|
255
|
+
sensors = {}
|
256
|
+
@inverters.product(@types).each do |source, type|
|
257
|
+
key = "inverter_#{source}_#{type}"
|
258
|
+
sensors[key] = InverterUnderlying.new(key, self)
|
259
|
+
end
|
260
|
+
sensors
|
261
|
+
end
|
262
|
+
|
263
|
+
private
|
264
|
+
def read_all
|
265
|
+
response = SAAL::do_http_get_digest(@host, 80, "/api/v1/production/inverters", @user, @password, @timeout)
|
266
|
+
return nil if !response
|
267
|
+
|
268
|
+
values = JSON.parse(response.body)
|
269
|
+
inverters = {}
|
270
|
+
values.each do |inverter|
|
271
|
+
{"lastReportDate" => "last_report_date",
|
272
|
+
"lastReportWatts" => "watts_now",
|
273
|
+
"maxReportWatts" => "watts_max",
|
274
|
+
}.each do |type, label|
|
275
|
+
inverters["inverter_#{inverter["serialNumber"]}_#{label}"] = inverter[type]
|
276
|
+
@inverters_list[inverter["serialNumber"]] = true
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
inverters
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
data/lib/http.rb
ADDED
@@ -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
|
data/lib/saal.rb
CHANGED
@@ -10,7 +10,7 @@ module SAAL
|
|
10
10
|
DBCONF = CONFDIR+"database.yml"
|
11
11
|
CHARTSCONF = CONFDIR+"charts.yml"
|
12
12
|
|
13
|
-
VERSION = '0.3.
|
13
|
+
VERSION = '0.3.1'
|
14
14
|
end
|
15
15
|
|
16
16
|
require File.dirname(__FILE__)+'/dbstore.rb'
|
@@ -23,4 +23,6 @@ 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'
|
26
28
|
|
data/lib/sensors.rb
CHANGED
@@ -40,9 +40,23 @@ 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
|
43
57
|
else
|
44
|
-
|
45
|
-
|
58
|
+
p defs, name
|
59
|
+
raise UnknownSensorType, "Couldn't figure out a valid sensor type for #{name}"
|
46
60
|
end
|
47
61
|
end
|
48
62
|
end
|
data/saal.gemspec
CHANGED
@@ -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.3.
|
10
|
-
s.date = '
|
9
|
+
s.version = '0.3.1'
|
10
|
+
s.date = '2020-12-19'
|
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
|
@@ -32,6 +32,7 @@ EOF
|
|
32
32
|
s.add_runtime_dependency 'ownet', "~>0.2"
|
33
33
|
s.add_runtime_dependency 'mysql2', "~>0.5"
|
34
34
|
s.add_runtime_dependency 'nokogiri', '~>1.8'
|
35
|
+
s.add_runtime_dependency 'net-http-digest_auth', '~>1.4'
|
35
36
|
|
36
37
|
# = MANIFEST =
|
37
38
|
s.files = %w[
|
@@ -45,6 +46,7 @@ EOF
|
|
45
46
|
bin/saal_chart
|
46
47
|
bin/saal_daemon
|
47
48
|
bin/saal_dump_database
|
49
|
+
bin/saal_envoy_read
|
48
50
|
bin/saal_import_mysql
|
49
51
|
bin/saal_readall
|
50
52
|
lib/chart.rb
|
@@ -53,6 +55,8 @@ EOF
|
|
53
55
|
lib/daemon.rb
|
54
56
|
lib/dbstore.rb
|
55
57
|
lib/dinrelay.rb
|
58
|
+
lib/envoy.rb
|
59
|
+
lib/http.rb
|
56
60
|
lib/outliercache.rb
|
57
61
|
lib/owsensor.rb
|
58
62
|
lib/saal.rb
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: saal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Pedro Côrte-Real
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-12-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ownet
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '1.8'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: net-http-digest_auth
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.4'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.4'
|
55
69
|
description: "A daemon and libraries to create an abstraction layer that interfaces
|
56
70
|
with \nsensors and actuators, recording their state, responding to requests \nfor
|
57
71
|
current and historical values, and allowing changes of state.\n"
|
@@ -60,12 +74,12 @@ executables:
|
|
60
74
|
- dinrelaystatus
|
61
75
|
- dinrelayset
|
62
76
|
- saal_import_mysql
|
63
|
-
- saal_chart~
|
64
|
-
- saal_daemon~
|
65
77
|
- saal_dump_database
|
66
78
|
- saal_readall
|
67
79
|
- saal_chart
|
68
80
|
- saal_daemon
|
81
|
+
- saal_envoy_generate_config
|
82
|
+
- saal_envoy_read
|
69
83
|
extensions: []
|
70
84
|
extra_rdoc_files:
|
71
85
|
- README.rdoc
|
@@ -79,10 +93,10 @@ files:
|
|
79
93
|
- bin/dinrelayset
|
80
94
|
- bin/dinrelaystatus
|
81
95
|
- bin/saal_chart
|
82
|
-
- bin/saal_chart~
|
83
96
|
- bin/saal_daemon
|
84
|
-
- bin/saal_daemon~
|
85
97
|
- bin/saal_dump_database
|
98
|
+
- bin/saal_envoy_generate_config
|
99
|
+
- bin/saal_envoy_read
|
86
100
|
- bin/saal_import_mysql
|
87
101
|
- bin/saal_readall
|
88
102
|
- lib/chart.rb
|
@@ -91,6 +105,8 @@ files:
|
|
91
105
|
- lib/daemon.rb
|
92
106
|
- lib/dbstore.rb
|
93
107
|
- lib/dinrelay.rb
|
108
|
+
- lib/envoy.rb
|
109
|
+
- lib/http.rb
|
94
110
|
- lib/outliercache.rb
|
95
111
|
- lib/owsensor.rb
|
96
112
|
- lib/saal.rb
|
@@ -118,7 +134,7 @@ homepage: https://github.com/pedrocr/saal
|
|
118
134
|
licenses:
|
119
135
|
- LGPL-2.1
|
120
136
|
metadata: {}
|
121
|
-
post_install_message:
|
137
|
+
post_install_message:
|
122
138
|
rdoc_options:
|
123
139
|
- "-S"
|
124
140
|
- "-w 2"
|
@@ -137,9 +153,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
137
153
|
- !ruby/object:Gem::Version
|
138
154
|
version: '0'
|
139
155
|
requirements: []
|
140
|
-
|
141
|
-
|
142
|
-
signing_key:
|
156
|
+
rubygems_version: 3.1.2
|
157
|
+
signing_key:
|
143
158
|
specification_version: 2
|
144
159
|
summary: Thin abstraction layer for interfacing and recording sensors (currently onewire)
|
145
160
|
and actuators (currently dinrelay)
|
data/bin/saal_chart~
DELETED
@@ -1,90 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
NUM_VALUES_SMALL = 500 # Datapoints in "small" charts
|
4
|
-
NUM_VALUES_LARGE = 150 # Datapoints in "large" charts
|
5
|
-
LARGE_CHART_THRESHOLD = 30*24*60*60 # Threshold for a large chart (in seconds)
|
6
|
-
|
7
|
-
require File.dirname(__FILE__)+'/../lib/saal.rb'
|
8
|
-
|
9
|
-
def usage
|
10
|
-
$stderr.puts("Usage: saal_chart <chart dir>")
|
11
|
-
end
|
12
|
-
|
13
|
-
if ARGV.size != 1
|
14
|
-
usage
|
15
|
-
exit (2)
|
16
|
-
end
|
17
|
-
|
18
|
-
SENSOR_RANGES = {:temperature=>[-15, 45], :humidity=>[0,100], :pressure=>[950,1050]}
|
19
|
-
|
20
|
-
|
21
|
-
SAAL::Charts.new.each do |chart|
|
22
|
-
$stderr.puts "Generating chart #{chart.name}"
|
23
|
-
|
24
|
-
pngfile = ARGV[0]+'/chart-'+chart.name.to_s+'.png'
|
25
|
-
ymlfile = ARGV[0]+'/chart-'+chart.name.to_s+'.yml'
|
26
|
-
|
27
|
-
@mins = chart.minimum
|
28
|
-
@maxs = chart.maximum
|
29
|
-
@avgs = chart.average
|
30
|
-
@minmax = {}
|
31
|
-
chart.sensors.each do |s|
|
32
|
-
s = s.name.to_sym
|
33
|
-
@minmax[s] = {:maximum => @maxs[s], :minimum => @mins[s], :average => @avgs[s]}
|
34
|
-
end
|
35
|
-
|
36
|
-
File.open(ymlfile, 'w').write(YAML::dump(@minmax))
|
37
|
-
|
38
|
-
def normalize_data(data, min, max)
|
39
|
-
data.map do |i|
|
40
|
-
if i.nil?
|
41
|
-
-1.0
|
42
|
-
elsif i < min
|
43
|
-
0.0
|
44
|
-
elsif i > max
|
45
|
-
100.0
|
46
|
-
else
|
47
|
-
(((i-min)/(max-min).to_f)*1000).round/10.0
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
@periodnames = chart.periodnames
|
53
|
-
@numperiods = @periodnames.size
|
54
|
-
num_values = ((chart.to-chart.from)>LARGE_CHART_THRESHOLD) ?
|
55
|
-
NUM_VALUES_LARGE :
|
56
|
-
NUM_VALUES_SMALL
|
57
|
-
|
58
|
-
@averages = chart.average(num_values)
|
59
|
-
|
60
|
-
@data = chart.sensors.map do |sensor|
|
61
|
-
normalize_data(@averages[sensor.name.to_sym], *(SENSOR_RANGES[sensor.sensor_type]))
|
62
|
-
end
|
63
|
-
|
64
|
-
@dataurl = @data.map {|values| values.join(",")}.join('|')
|
65
|
-
|
66
|
-
r = {}
|
67
|
-
case chart.alignlabels
|
68
|
-
when :center
|
69
|
-
@periodnamesurl = "||"+@periodnames.join('||')+"||"
|
70
|
-
when :left
|
71
|
-
@periodnamesurl = "|"+@periodnames.join('|')+"||"
|
72
|
-
r[:chxs] = "0,555555,11,-1,lt"
|
73
|
-
end
|
74
|
-
@xincr = 100.0/@numperiods.to_f*10000.truncate.to_f/10000
|
75
|
-
|
76
|
-
r[:chof] = "png"
|
77
|
-
r[:chs] = "700x300"
|
78
|
-
r[:cht] = "lc"
|
79
|
-
r[:chco] = "00ff00,ff0000,0000ff,ff9933,800080"
|
80
|
-
r[:chxt] = "x,y,y,r"
|
81
|
-
r[:chxl] = "0:#{@periodnamesurl}1:|-15ºC||0||15||30||45ºC|2:|0%|25|50|75|100%|3:|950||975||1000||1025||1050 hPa"
|
82
|
-
r[:chg] = "#{@xincr},12.5,1,5"
|
83
|
-
r[:chd] = "t:#{@dataurl}"
|
84
|
-
|
85
|
-
@url = "http://chart.apis.google.com/chart?&"
|
86
|
-
@postdata = r.map{|k,v| k.to_s+"="+v}.join("&")
|
87
|
-
|
88
|
-
|
89
|
-
system "wget --quiet \"#{@url}\" --post-data=\"#{@postdata}\" -O #{pngfile}"
|
90
|
-
end
|
data/bin/saal_daemon~
DELETED
@@ -1,31 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
SENSOR_INTERVAL = 60 # seconds between consecutive measurements of the sensors
|
4
|
-
DBCONF = "/etc/saal/database.yml"
|
5
|
-
SENSORSCONF = "/etc/saal/sensors.yml"
|
6
|
-
|
7
|
-
require File.dirname(__FILE__)+'/../lib/saal.rb'
|
8
|
-
|
9
|
-
def usage
|
10
|
-
$stderr.puts "Usage: saal_daemon <pidfile|--foreground>"
|
11
|
-
end
|
12
|
-
|
13
|
-
if ARGV.size != 1
|
14
|
-
usage
|
15
|
-
exit 2
|
16
|
-
else
|
17
|
-
pidfile = ARGV[0]
|
18
|
-
foreground = (ARGV[0] == '--foreground')
|
19
|
-
d = SAAL::Daemon.new(:interval => SENSOR_INTERVAL,
|
20
|
-
:sensorconf => SENSORSCONF,
|
21
|
-
:dbconf => DBCONF,
|
22
|
-
:foreground => foreground,
|
23
|
-
:keep_stdin => true)
|
24
|
-
pid = d.run
|
25
|
-
if !foreground
|
26
|
-
File.open(pidfile, 'w') do |f|
|
27
|
-
f.write(pid)
|
28
|
-
f.close
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|