saal 0.2.22 → 0.3.1
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 +7 -0
- data/bin/saal_chart +50 -4
- data/bin/saal_envoy_generate_config +53 -0
- data/bin/saal_envoy_read +143 -0
- data/lib/chart.rb +1 -1
- data/lib/chart_data.rb +1 -1
- data/lib/dbstore.rb +15 -18
- data/lib/dinrelay.rb +2 -21
- data/lib/envoy.rb +284 -0
- data/lib/http.rb +51 -0
- data/lib/outliercache.rb +1 -1
- data/lib/saal.rb +4 -2
- data/lib/sensors.rb +16 -2
- data/saal.gemspec +10 -5
- data/test/chart_data_test.rb +1 -1
- data/test/chart_test.rb +0 -3
- data/test/daemon_test.rb +2 -2
- data/test/dbstore_test.rb +5 -2
- data/test/dinrelay_test.rb +1 -1
- data/test/sensors_test.rb +5 -5
- data/test/test_db.yml +3 -3
- data/test/test_helper.rb +1 -2
- data/test/test_sensors.yml +1 -1
- metadata +65 -42
- data/bin/saal_chart~ +0 -90
- data/bin/saal_daemon~ +0 -31
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 62d7f9b51083356448d50bcb5ad66c729d84a38e152f85470ca4fa5f9f5fed15
|
4
|
+
data.tar.gz: 73f51e4736d14ee249531535577a8ff0568e0c19fb3115a4cf76bb0801d8e504
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 349eed054b1fd37de656840840b92e1be279e77fe677b92cc47864fe96079f88e6dc784e608bf5a7843b08cc0025b1f141d1bf88be1cef553e18f104cc0c3d74
|
7
|
+
data.tar.gz: e07a22761c8f4f6c01d631abb29b01c63d6888c449fd67762e558ddd7693cd1bdfdf5b8bd7cdc35575006c6cb2fc9cbef49329bf08b754d5101d5dfc20d3421d
|
data/bin/saal_chart
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
# encoding: utf-8
|
2
3
|
|
3
4
|
NUM_VALUES_SMALL = 500 # Datapoints in "small" charts
|
4
5
|
NUM_VALUES_LARGE = 150 # Datapoints in "large" charts
|
@@ -15,7 +16,11 @@ if ARGV.size != 1
|
|
15
16
|
exit (2)
|
16
17
|
end
|
17
18
|
|
18
|
-
|
19
|
+
MAX_RANGES = {:temperature=>[-10,70],
|
20
|
+
:humidity=>[0,110],
|
21
|
+
:pressure=>[900,1200]}
|
22
|
+
|
23
|
+
TYPES = {:temperature => "ºC", :humidity => "%", :pressure => "hPa"}
|
19
24
|
|
20
25
|
|
21
26
|
SAAL::Charts.new.each do |chart|
|
@@ -56,9 +61,35 @@ SAAL::Charts.new.each do |chart|
|
|
56
61
|
NUM_VALUES_SMALL
|
57
62
|
|
58
63
|
@averages = chart.average(num_values)
|
64
|
+
@ranges = {}
|
65
|
+
|
66
|
+
# First find the smallest interval that fits all sensors for each type
|
67
|
+
@data = chart.sensors.map do |sensor|
|
68
|
+
avgs = @averages[sensor.name.to_sym]
|
69
|
+
|
70
|
+
min = avgs.select{|o| o != nil}.min
|
71
|
+
max = avgs.select{|o| o != nil}.max
|
72
|
+
if min and max
|
73
|
+
range = [(min/5).floor*5, (max/5).ceil*5]
|
74
|
+
@ranges[sensor.sensor_type] ||= range
|
75
|
+
previous = @ranges[sensor.sensor_type]
|
76
|
+
@ranges[sensor.sensor_type] = [[range[0],previous[0]].min,
|
77
|
+
[range[1],previous[1]].max]
|
78
|
+
end
|
79
|
+
end
|
59
80
|
|
81
|
+
# Then clip those intervals to MAX_RANGES
|
60
82
|
@data = chart.sensors.map do |sensor|
|
61
|
-
|
83
|
+
maxrange = MAX_RANGES[sensor.sensor_type]
|
84
|
+
@ranges[sensor.sensor_type] ||= maxrange
|
85
|
+
previous = @ranges[sensor.sensor_type]
|
86
|
+
@ranges[sensor.sensor_type] = [[maxrange[0],previous[0]].max,
|
87
|
+
[maxrange[1],previous[1]].min]
|
88
|
+
end
|
89
|
+
|
90
|
+
# Finally use those intervals to normalize the data
|
91
|
+
@data = chart.sensors.map do |sensor|
|
92
|
+
normalize_data(@averages[sensor.name.to_sym], *@ranges[sensor.sensor_type])
|
62
93
|
end
|
63
94
|
|
64
95
|
@dataurl = @data.map {|values| values.join(",")}.join('|')
|
@@ -73,12 +104,27 @@ SAAL::Charts.new.each do |chart|
|
|
73
104
|
end
|
74
105
|
@xincr = 100.0/@numperiods.to_f*10000.truncate.to_f/10000
|
75
106
|
|
107
|
+
@axes = []
|
108
|
+
@ranges.each do |type, range|
|
109
|
+
min,max = range
|
110
|
+
step = (max-min).to_f/4.0
|
111
|
+
steps = (0..4).map{|i| (min+i*step).to_i.to_s}
|
112
|
+
steps[0] = "#{min} #{TYPES[type]}"
|
113
|
+
steps[4] = "#{max} #{TYPES[type]}"
|
114
|
+
@axes << steps.join("||")
|
115
|
+
end
|
116
|
+
# Duplicate the axis if there's only one
|
117
|
+
@axes *= 2 if @axes.size == 1
|
118
|
+
# Alternate between left and right axes
|
119
|
+
@axisset = (['y','r']*@axes.size)[0..@axes.size-1].join(",")
|
120
|
+
@axes = @axes.each_with_index.map {|a,i| "#{i+1}:|#{a}"}.join("|")
|
121
|
+
|
76
122
|
r[:chof] = "png"
|
77
123
|
r[:chs] = "700x300"
|
78
124
|
r[:cht] = "lc"
|
79
125
|
r[:chco] = "00ff00,ff0000,0000ff,ff9933,800080"
|
80
|
-
r[:chxt] = "x
|
81
|
-
r[:chxl] = "0:#{@periodnamesurl}
|
126
|
+
r[:chxt] = "x,#{@axisset}"
|
127
|
+
r[:chxl] = "0:#{@periodnamesurl}#{@axes}"
|
82
128
|
r[:chg] = "#{@xincr},12.5,1,5"
|
83
129
|
r[:chd] = "t:#{@dataurl}"
|
84
130
|
|
@@ -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/chart.rb
CHANGED
@@ -5,7 +5,7 @@ module SAAL
|
|
5
5
|
@name = name
|
6
6
|
@defs = defs
|
7
7
|
@alignlabels = (defs['alignlabels'] || :center).to_sym
|
8
|
-
@sensors = defs['sensors'].map{|
|
8
|
+
@sensors = defs['sensors'].map{|sname| sensors.send(sname)}
|
9
9
|
@num = defs['last']
|
10
10
|
@periods = defs['periods']
|
11
11
|
@alt = defs['alt']
|
data/lib/chart_data.rb
CHANGED
data/lib/dbstore.rb
CHANGED
@@ -46,54 +46,51 @@ module SAAL
|
|
46
46
|
WHERE sensor = '#{db_quote(sensor.to_s)}'
|
47
47
|
AND date > '#{Time.now.utc.to_i - MAX_LAST_VAL_AGE}'
|
48
48
|
ORDER BY date DESC LIMIT 1" do |r|
|
49
|
-
|
50
|
-
|
49
|
+
row = r.first
|
50
|
+
if row
|
51
|
+
_date, value = [row["date"].to_i, row["value"].to_f]
|
52
|
+
value
|
51
53
|
else
|
52
|
-
|
53
|
-
date, value = [row[0].to_i, row[1].to_f]
|
54
|
-
return value
|
54
|
+
nil
|
55
55
|
end
|
56
56
|
end
|
57
57
|
end
|
58
58
|
|
59
59
|
def each
|
60
60
|
db_query "SELECT sensor,date,value FROM sensor_reads" do |r|
|
61
|
-
r.
|
62
|
-
row
|
63
|
-
yield [row[0],row[1].to_i, row[2].to_f]
|
61
|
+
r.each do |row|
|
62
|
+
yield [row["sensor"],row["date"].to_i, row["value"].to_f]
|
64
63
|
end
|
65
64
|
end
|
66
65
|
end
|
67
66
|
|
68
67
|
private
|
69
68
|
def db_range(function, sensor, from, to)
|
70
|
-
db_query "SELECT #{function}(value) AS
|
69
|
+
db_query "SELECT #{function}(value) AS func FROM sensor_reads
|
71
70
|
WHERE sensor = '#{db_quote(sensor.to_s)}'
|
72
71
|
AND date >= #{from.to_s}
|
73
72
|
AND date <= #{to.to_s}" do |r|
|
74
|
-
|
75
|
-
|
73
|
+
row = r.first
|
74
|
+
if row && row["func"]
|
75
|
+
row["func"].to_f
|
76
76
|
else
|
77
|
-
|
78
|
-
row[0] ? row[0].to_f : nil
|
77
|
+
nil
|
79
78
|
end
|
80
79
|
end
|
81
80
|
end
|
82
81
|
|
83
82
|
def db_quote(text)
|
84
|
-
|
83
|
+
Mysql2::Client.escape(text)
|
85
84
|
end
|
86
85
|
|
87
86
|
def db_query(query, opts={})
|
88
87
|
db = nil
|
89
88
|
begin
|
90
89
|
# connect to the MySQL server
|
91
|
-
db =
|
92
|
-
@dbopts['db'],@dbopts['port'],@dbopts['socket'],
|
93
|
-
@dbopts['flags'])
|
90
|
+
db = Mysql2::Client.new(@dbopts)
|
94
91
|
res = db.query(query)
|
95
92
|
yield res if block_given?
|
96
|
-
rescue
|
93
|
+
rescue Mysql2::Error => e
|
97
94
|
$stderr.puts "MySQL Error #{e.errno}: #{e.error}" if !(e.errno == opts[:ignoreerr])
|
98
95
|
ensure
|
99
96
|
db.close if db
|
data/lib/dinrelay.rb
CHANGED
@@ -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)
|
@@ -55,33 +56,13 @@ module SAAL
|
|
55
56
|
def set_state(num, state)
|
56
57
|
@cachetime = nil
|
57
58
|
response = do_get("/outlet?#{num}=#{state}")
|
58
|
-
#FIXME: Find a better workaround for dinrelay's crashing when you cycle
|
59
|
-
# through outlets too fast
|
60
|
-
sleep 1
|
61
59
|
response != nil
|
62
60
|
end
|
63
61
|
|
64
62
|
private
|
65
63
|
def do_get(path)
|
66
|
-
|
67
|
-
http = Net::HTTP.new(@host,@port)
|
68
|
-
# Timeout faster when the other side doesn't respond
|
69
|
-
http.open_timeout = @timeout
|
70
|
-
http.read_timeout = @timeout
|
71
|
-
req = Net::HTTP::Get.new(path)
|
72
|
-
req.basic_auth @user, @pass
|
73
|
-
response = http.request(req)
|
74
|
-
if response.code != "200"
|
75
|
-
#$stderr.puts "ERROR: Code #{response.code}"
|
76
|
-
#$stderr.puts response.body
|
77
|
-
return nil
|
78
|
-
end
|
79
|
-
return response
|
80
|
-
rescue Exception
|
81
|
-
return nil
|
82
|
-
end
|
64
|
+
SAAL::do_http_get(@host, @port, path, @user, @pass, @timeout)
|
83
65
|
end
|
84
|
-
|
85
66
|
def parse_index_html(str)
|
86
67
|
doc = Nokogiri::HTML(str)
|
87
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/outliercache.rb
CHANGED
@@ -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){|
|
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|
|
data/lib/saal.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'yaml'
|
2
|
-
require "
|
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.
|
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.
|
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
|
@@ -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.
|
32
|
-
s.
|
33
|
-
s.
|
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[
|
@@ -44,6 +46,7 @@ EOF
|
|
44
46
|
bin/saal_chart
|
45
47
|
bin/saal_daemon
|
46
48
|
bin/saal_dump_database
|
49
|
+
bin/saal_envoy_read
|
47
50
|
bin/saal_import_mysql
|
48
51
|
bin/saal_readall
|
49
52
|
lib/chart.rb
|
@@ -52,6 +55,8 @@ EOF
|
|
52
55
|
lib/daemon.rb
|
53
56
|
lib/dbstore.rb
|
54
57
|
lib/dinrelay.rb
|
58
|
+
lib/envoy.rb
|
59
|
+
lib/http.rb
|
55
60
|
lib/outliercache.rb
|
56
61
|
lib/owsensor.rb
|
57
62
|
lib/saal.rb
|
data/test/chart_data_test.rb
CHANGED
@@ -78,7 +78,7 @@ class TestChartData < Test::Unit::TestCase
|
|
78
78
|
Time.utc(2012, 3, 25, 23, 59, 59),
|
79
79
|
["0"]+(2..23).map{|s| s.to_s}+["0"],
|
80
80
|
Time.utc(2012, 3, 25, 23, 59, 59),
|
81
|
-
"
|
81
|
+
"_changing_timezone_test2","Europe/Lisbon")
|
82
82
|
|
83
83
|
assert_alignment_interval(24, :hours, Time.utc(2010, 12, 29, 16, 0, 0),
|
84
84
|
Time.utc(2010, 12, 30, 15, 59, 59),
|
data/test/chart_test.rb
CHANGED
@@ -16,7 +16,6 @@ class TestChart < Test::Unit::TestCase
|
|
16
16
|
|
17
17
|
def test_average
|
18
18
|
name = 'week'
|
19
|
-
defs = @defs[name]
|
20
19
|
chart = @charts.find(name)
|
21
20
|
assert_equal ['Fri','Sat','Sun','Mon','Tue','Wed','Thu'], chart.periodnames
|
22
21
|
chart.sensors.each {|s| s.mock_set(:average => 1)}
|
@@ -31,7 +30,6 @@ class TestChart < Test::Unit::TestCase
|
|
31
30
|
|
32
31
|
def test_min_max_avg_1arity
|
33
32
|
name = 'week'
|
34
|
-
defs = @defs[name]
|
35
33
|
chart = @charts.find(name)
|
36
34
|
assert_equal ['Fri','Sat','Sun','Mon','Tue','Wed','Thu'], chart.periodnames
|
37
35
|
v = {:minimum => 1.0, :maximum => 2.0, :average => 1.5}
|
@@ -43,7 +41,6 @@ class TestChart < Test::Unit::TestCase
|
|
43
41
|
|
44
42
|
def test_min_max_0arity
|
45
43
|
name = 'week'
|
46
|
-
defs = @defs[name]
|
47
44
|
chart = @charts.find(name)
|
48
45
|
assert_equal ['Fri','Sat','Sun','Mon','Tue','Wed','Thu'], chart.periodnames
|
49
46
|
v = {:minimum => 1.0, :maximum => 2.0, :average => 1.5}
|
data/test/daemon_test.rb
CHANGED
@@ -18,7 +18,7 @@ class TestDaemon < Test::Unit::TestCase
|
|
18
18
|
end
|
19
19
|
|
20
20
|
db_test_query("SELECT * FROM sensor_reads") do |res|
|
21
|
-
assert res.
|
21
|
+
assert res.count > 0, "No sensor reads in DB"
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
@@ -38,7 +38,7 @@ class TestDaemon < Test::Unit::TestCase
|
|
38
38
|
end
|
39
39
|
|
40
40
|
db_test_query("SELECT * FROM sensor_reads") do |res|
|
41
|
-
assert res.
|
41
|
+
assert res.count == 0
|
42
42
|
end
|
43
43
|
end
|
44
44
|
end
|
data/test/dbstore_test.rb
CHANGED
@@ -9,8 +9,11 @@ class TestFileStore < Test::Unit::TestCase
|
|
9
9
|
@dbstore.write(:test_sensor, test_time, test_value)
|
10
10
|
|
11
11
|
db_test_query("SELECT * FROM sensor_reads") do |res|
|
12
|
-
assert_equal 1, res.
|
13
|
-
|
12
|
+
assert_equal 1, res.count
|
13
|
+
row = res.first
|
14
|
+
assert_equal "test_sensor", row["sensor"]
|
15
|
+
assert_equal test_time, row["date"].to_i
|
16
|
+
assert_equal test_value, row["value"].to_f
|
14
17
|
end
|
15
18
|
end
|
16
19
|
|
data/test/dinrelay_test.rb
CHANGED
@@ -151,7 +151,7 @@ class TestDINRelay < Test::Unit::TestCase
|
|
151
151
|
|
152
152
|
# Test that the cache times out
|
153
153
|
def test_cache_invalidation
|
154
|
-
|
154
|
+
_sensors = SAAL::Sensors.new(@test_sensors_dinrelay_file, TEST_DBCONF)
|
155
155
|
@og.cache_timeout = 0.1
|
156
156
|
with_webrick(:html=>create_index_html(@vals)) do |feedback|
|
157
157
|
@og.state(1)
|
data/test/sensors_test.rb
CHANGED
@@ -67,11 +67,11 @@ class TestSensors < Test::Unit::TestCase
|
|
67
67
|
end
|
68
68
|
|
69
69
|
db_test_query("SELECT * FROM sensor_reads") do |res|
|
70
|
-
assert_equal 1, res.
|
71
|
-
row = res.
|
72
|
-
assert_equal "fake_temp", row[
|
73
|
-
assert_in_delta Time.now.utc.to_i, row[
|
74
|
-
assert_instance_of Float, row[
|
70
|
+
assert_equal 1, res.count
|
71
|
+
row = res.first
|
72
|
+
assert_equal "fake_temp", row["sensor"]
|
73
|
+
assert_in_delta Time.now.utc.to_i, row["date"].to_i, 100
|
74
|
+
assert_instance_of Float, row["value"].to_f
|
75
75
|
end
|
76
76
|
end
|
77
77
|
end
|
data/test/test_db.yml
CHANGED
@@ -1,4 +1,4 @@
|
|
1
1
|
host: localhost
|
2
|
-
|
3
|
-
|
4
|
-
|
2
|
+
username: sensor_reads
|
3
|
+
password: password
|
4
|
+
database: sensor_reads_test
|
data/test/test_helper.rb
CHANGED
data/test/test_sensors.yml
CHANGED
metadata
CHANGED
@@ -1,63 +1,85 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: saal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
5
|
-
prerelease:
|
4
|
+
version: 0.3.1
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Pedro Côrte-Real
|
9
|
-
autorequire:
|
8
|
+
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date:
|
11
|
+
date: 2020-12-19 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
14
|
name: ownet
|
16
|
-
requirement:
|
17
|
-
none: false
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
18
16
|
requirements:
|
19
|
-
- -
|
17
|
+
- - "~>"
|
20
18
|
- !ruby/object:Gem::Version
|
21
|
-
version: 0.2
|
19
|
+
version: '0.2'
|
22
20
|
type: :runtime
|
23
21
|
prerelease: false
|
24
|
-
version_requirements:
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: mysql2
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.5'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.5'
|
25
41
|
- !ruby/object:Gem::Dependency
|
26
42
|
name: nokogiri
|
27
|
-
requirement:
|
28
|
-
none: false
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
29
44
|
requirements:
|
30
|
-
- -
|
45
|
+
- - "~>"
|
31
46
|
- !ruby/object:Gem::Version
|
32
|
-
version: '
|
47
|
+
version: '1.8'
|
33
48
|
type: :runtime
|
34
49
|
prerelease: false
|
35
|
-
version_requirements:
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.8'
|
36
55
|
- !ruby/object:Gem::Dependency
|
37
|
-
name:
|
38
|
-
requirement:
|
39
|
-
none: false
|
56
|
+
name: net-http-digest_auth
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
40
58
|
requirements:
|
41
|
-
- -
|
59
|
+
- - "~>"
|
42
60
|
- !ruby/object:Gem::Version
|
43
|
-
version: '
|
61
|
+
version: '1.4'
|
44
62
|
type: :runtime
|
45
63
|
prerelease: false
|
46
|
-
version_requirements:
|
47
|
-
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.4'
|
69
|
+
description: "A daemon and libraries to create an abstraction layer that interfaces
|
48
70
|
with \nsensors and actuators, recording their state, responding to requests \nfor
|
49
71
|
current and historical values, and allowing changes of state.\n"
|
50
72
|
email: pedro@pedrocr.net
|
51
73
|
executables:
|
52
|
-
- saal_chart~
|
53
74
|
- dinrelaystatus
|
54
|
-
- saal_readall
|
55
|
-
- saal_dump_database
|
56
|
-
- saal_daemon
|
57
|
-
- saal_chart
|
58
|
-
- saal_daemon~
|
59
75
|
- dinrelayset
|
60
76
|
- saal_import_mysql
|
77
|
+
- saal_dump_database
|
78
|
+
- saal_readall
|
79
|
+
- saal_chart
|
80
|
+
- saal_daemon
|
81
|
+
- saal_envoy_generate_config
|
82
|
+
- saal_envoy_read
|
61
83
|
extensions: []
|
62
84
|
extra_rdoc_files:
|
63
85
|
- README.rdoc
|
@@ -73,6 +95,8 @@ files:
|
|
73
95
|
- bin/saal_chart
|
74
96
|
- bin/saal_daemon
|
75
97
|
- bin/saal_dump_database
|
98
|
+
- bin/saal_envoy_generate_config
|
99
|
+
- bin/saal_envoy_read
|
76
100
|
- bin/saal_import_mysql
|
77
101
|
- bin/saal_readall
|
78
102
|
- lib/chart.rb
|
@@ -81,6 +105,8 @@ files:
|
|
81
105
|
- lib/daemon.rb
|
82
106
|
- lib/dbstore.rb
|
83
107
|
- lib/dinrelay.rb
|
108
|
+
- lib/envoy.rb
|
109
|
+
- lib/http.rb
|
84
110
|
- lib/outliercache.rb
|
85
111
|
- lib/owsensor.rb
|
86
112
|
- lib/saal.rb
|
@@ -104,34 +130,31 @@ files:
|
|
104
130
|
- test/test_helper.rb
|
105
131
|
- test/test_sensor_cleanups.yml
|
106
132
|
- test/test_sensors.yml
|
107
|
-
- bin/saal_chart~
|
108
|
-
- bin/saal_daemon~
|
109
133
|
homepage: https://github.com/pedrocr/saal
|
110
|
-
licenses:
|
111
|
-
|
134
|
+
licenses:
|
135
|
+
- LGPL-2.1
|
136
|
+
metadata: {}
|
137
|
+
post_install_message:
|
112
138
|
rdoc_options:
|
113
|
-
- -S
|
114
|
-
- -w 2
|
115
|
-
- -N
|
116
|
-
- -c utf8
|
139
|
+
- "-S"
|
140
|
+
- "-w 2"
|
141
|
+
- "-N"
|
142
|
+
- "-c utf8"
|
117
143
|
require_paths:
|
118
144
|
- lib
|
119
145
|
required_ruby_version: !ruby/object:Gem::Requirement
|
120
|
-
none: false
|
121
146
|
requirements:
|
122
|
-
- -
|
147
|
+
- - ">="
|
123
148
|
- !ruby/object:Gem::Version
|
124
149
|
version: '0'
|
125
150
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
126
|
-
none: false
|
127
151
|
requirements:
|
128
|
-
- -
|
152
|
+
- - ">="
|
129
153
|
- !ruby/object:Gem::Version
|
130
154
|
version: '0'
|
131
155
|
requirements: []
|
132
|
-
|
133
|
-
|
134
|
-
signing_key:
|
156
|
+
rubygems_version: 3.1.2
|
157
|
+
signing_key:
|
135
158
|
specification_version: 2
|
136
159
|
summary: Thin abstraction layer for interfacing and recording sensors (currently onewire)
|
137
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,ffff00,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
|