saal 0.2.25 → 0.3.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/saal_denkovi_relays +34 -0
- data/bin/saal_envoy_generate_config +53 -0
- data/bin/saal_envoy_read +152 -0
- data/lib/chart.rb +1 -1
- data/lib/chart_data.rb +1 -1
- data/lib/dbstore.rb +41 -18
- data/lib/denkovi.rb +101 -0
- data/lib/dinrelay.rb +3 -19
- data/lib/envoy.rb +287 -0
- data/lib/http.rb +51 -0
- data/lib/outliercache.rb +1 -1
- data/lib/saal.rb +5 -2
- data/lib/sensor.rb +5 -0
- data/lib/sensors.rb +31 -2
- data/saal.gemspec +16 -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 +20 -2
- data/test/denkovi.json.erb +39 -0
- data/test/denkovi_test.rb +212 -0
- data/test/dinrelay_test.rb +1 -1
- data/test/sensor_test.rb +7 -1
- data/test/sensors_test.rb +5 -5
- data/test/test_db.yml +3 -3
- data/test/test_denkovi_sensors.yml +39 -0
- data/test/test_helper.rb +2 -2
- data/test/test_sensors.yml +1 -1
- metadata +72 -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: 1a86991bf6a2c0ddf03cab4c239953c2c4e819f5019145398f4b7a527e928852
|
4
|
+
data.tar.gz: de08c727c732e4afb86cafcdcadfeec1f16fb127d675714b03bf388956bc6646
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: db0fe9ccfd6e51da7bec292c9cd44c0b8c994ca238772cd817941f22f823f8ac8098ef005373e2f86dfcd9d121dc6470bdd3615ba961d1c514c620e8cf399095
|
7
|
+
data.tar.gz: fa64aa3d8cea43fc12e07db8d84def9ba45ca27655dbb95251e7b7c45e8c322b5c8ee0f58b02b4c63c9799f15a5db1316e0e174a6b246ee4725887d3ce12d823
|
@@ -0,0 +1,34 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.dirname(__FILE__)+'/../lib/saal.rb'
|
4
|
+
|
5
|
+
def usage
|
6
|
+
$stderr.puts "USAGE: saal_denkovi_relays <host>"
|
7
|
+
$stderr.puts "USAGE: saal_denkovi_relays <host> <num> <ON/OFF>"
|
8
|
+
end
|
9
|
+
|
10
|
+
if ARGV.size != 1 && ARGV.size != 3
|
11
|
+
usage()
|
12
|
+
exit(1)
|
13
|
+
end
|
14
|
+
|
15
|
+
denkovi = SAAL::Denkovi::OutletGroup::new(
|
16
|
+
:host => ARGV[0],
|
17
|
+
)
|
18
|
+
|
19
|
+
if ARGV.size == 3
|
20
|
+
num = ARGV[1].to_i
|
21
|
+
newstate = ARGV[2]
|
22
|
+
if !["ON","OFF"].include? newstate
|
23
|
+
$stderr.puts "ERROR: Unknown state '#{newstate}'"
|
24
|
+
usage()
|
25
|
+
exit(1)
|
26
|
+
end
|
27
|
+
puts "Setting Relay #{num} to #{newstate}"
|
28
|
+
denkovi.set_state(num, newstate)
|
29
|
+
end
|
30
|
+
|
31
|
+
(1..16).each do |num|
|
32
|
+
puts "Relay #{num} is #{denkovi.state(num)}"
|
33
|
+
end
|
34
|
+
|
@@ -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,152 @@
|
|
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 fdispd(val)
|
35
|
+
if val
|
36
|
+
Time.at(val.to_i).strftime("%Y-%m-%d %k:%M:%S")
|
37
|
+
else
|
38
|
+
"n/a"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def l(vals, name)
|
43
|
+
sensor = vals[name]
|
44
|
+
if sensor
|
45
|
+
sensor.read
|
46
|
+
else
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def lratio(vals, name1, name2)
|
52
|
+
v1 = l(vals, name1)
|
53
|
+
v2 = l(vals, name2)
|
54
|
+
if v1 && v2
|
55
|
+
v1 / v2
|
56
|
+
else
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
ac_quality = SAAL::Envoy::ACQuality::new(:host => ARGV[0]).create_sensors
|
62
|
+
|
63
|
+
puts " ========= AC QUALITY ========"
|
64
|
+
puts " voltage (V) freq (Hz)"
|
65
|
+
def qual_line(vals,name, type)
|
66
|
+
puts " #{name} \
|
67
|
+
#{fdisp_dec(l(vals,"ac_#{type}_voltage"))} \
|
68
|
+
#{fdisp_dec(l(vals,"ac_#{type}_frequency"))} \
|
69
|
+
"
|
70
|
+
end
|
71
|
+
qual_line(ac_quality, "Total: ", "total")
|
72
|
+
qual_line(ac_quality, "Phase1:", "phase1")
|
73
|
+
qual_line(ac_quality, "Phase2:", "phase2")
|
74
|
+
qual_line(ac_quality, "Phase3:", "phase3")
|
75
|
+
|
76
|
+
production = SAAL::Envoy::PowerEnergy::new(:host => ARGV[0]).create_sensors
|
77
|
+
|
78
|
+
puts ""
|
79
|
+
puts " ============ TRUE POWER (W) ============ ======= TRUE ENERGY (kWh) ======="
|
80
|
+
puts " consumption production net consumption production net"
|
81
|
+
def p_line(vals, name, type, metric)
|
82
|
+
puts " #{name} \
|
83
|
+
#{fdisp(l(vals,"pv_total_consumption_#{type}_#{metric}_now"))} \
|
84
|
+
#{fdisp(l(vals,"pv_production_#{type}_#{metric}_now"))} \
|
85
|
+
#{fdisp(l(vals,"pv_net_consumption_#{type}_#{metric}_now"))} \
|
86
|
+
#{fdispk(l(vals,"pv_total_consumption_#{type}_#{metric}h_lifetime"))} \
|
87
|
+
#{fdispk(l(vals,"pv_production_#{type}_#{metric}h_lifetime"))} \
|
88
|
+
#{fdispk(l(vals,"pv_net_consumption_#{type}_#{metric}h_lifetime"))} \
|
89
|
+
"
|
90
|
+
end
|
91
|
+
p_line(production, "Total: ", "total", "w")
|
92
|
+
p_line(production, "Phase1:", "phase1", "w")
|
93
|
+
p_line(production, "Phase2:", "phase2", "w")
|
94
|
+
p_line(production, "Phase3:", "phase3", "w")
|
95
|
+
puts " Total Inverters: \
|
96
|
+
#{fdisp(l(production,"pv_production_inverters_w_now"))} \
|
97
|
+
\
|
98
|
+
#{fdispk(l(production,"pv_production_inverters_wh_lifetime"))} \
|
99
|
+
"
|
100
|
+
|
101
|
+
puts ""
|
102
|
+
puts " ========== APPARENT POWER (VA) ========= ===== APPARENT ENERGY (kVAh) ===="
|
103
|
+
puts " consumption production net consumption production net"
|
104
|
+
p_line(production, "Total: ", "total", "va")
|
105
|
+
p_line(production, "Phase1:", "phase1", "va")
|
106
|
+
p_line(production, "Phase2:", "phase2", "va")
|
107
|
+
p_line(production, "Phase3:", "phase3", "va")
|
108
|
+
|
109
|
+
def pf_line(vals, name, type, metric)
|
110
|
+
pf_total_consumption_instant = lratio(vals,"pv_total_consumption_#{type}_w_now","pv_total_consumption_#{type}_va_now")
|
111
|
+
pf_total_production_instant = lratio(vals,"pv_production_#{type}_w_now","pv_production_#{type}_va_now")
|
112
|
+
pf_net_production_instant = lratio(vals,"pv_net_consumption_#{type}_w_now","pv_net_consumption_#{type}_va_now")
|
113
|
+
|
114
|
+
pf_total_consumption_lifetime = lratio(vals,"pv_total_consumption_#{type}_wh_lifetime","pv_total_consumption_#{type}_vah_lifetime")
|
115
|
+
pf_total_production_lifetime = lratio(vals,"pv_production_#{type}_wh_lifetime","pv_production_#{type}_vah_lifetime")
|
116
|
+
pf_net_production_lifetime = lratio(vals,"pv_net_consumption_#{type}_wh_lifetime","pv_net_consumption_#{type}_vah_lifetime")
|
117
|
+
|
118
|
+
puts " #{name} \
|
119
|
+
#{fdisp_dec(pf_total_consumption_instant)} \
|
120
|
+
#{fdisp_dec(pf_total_production_instant)} \
|
121
|
+
#{fdisp_dec(pf_net_production_instant)} \
|
122
|
+
#{fdisp_dec(pf_total_consumption_lifetime)} \
|
123
|
+
#{fdisp_dec(pf_total_production_lifetime)} \
|
124
|
+
#{fdisp_dec(pf_net_production_lifetime)} \
|
125
|
+
"
|
126
|
+
end
|
127
|
+
|
128
|
+
puts ""
|
129
|
+
puts " ========= INSTANT POWER FACTOR ========= ====== LIFETIME POWER FACTOR ====="
|
130
|
+
puts " consumption production net consumption production net"
|
131
|
+
pf_line(production, "Total: ", "total", "va")
|
132
|
+
pf_line(production, "Phase1:", "phase1", "va")
|
133
|
+
pf_line(production, "Phase2:", "phase2", "va")
|
134
|
+
pf_line(production, "Phase3:", "phase3", "va")
|
135
|
+
|
136
|
+
puts ""
|
137
|
+
envoy = SAAL::Envoy::Inverters::new(
|
138
|
+
:host => ARGV[0],
|
139
|
+
:user => ARGV[1],
|
140
|
+
:password => ARGV[2],
|
141
|
+
:types => ["w_now", "last_report_date", "w_max"],
|
142
|
+
)
|
143
|
+
envoy.set_all_inverters!
|
144
|
+
inverters = envoy.create_sensors
|
145
|
+
puts " Found #{envoy.inverters.size} inverters"
|
146
|
+
envoy.inverters.each do |serial|
|
147
|
+
puts " INVERTER: #{serial} \
|
148
|
+
date:#{fdispd(l(inverters,"inverters_#{serial}_last_report_date"))} \
|
149
|
+
lastWatts:#{l(inverters,"inverters_#{serial}_w_now")} \
|
150
|
+
maxWatts:#{l(inverters,"inverters_#{serial}_w_max")} \
|
151
|
+
"
|
152
|
+
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
@@ -35,6 +35,32 @@ module SAAL
|
|
35
35
|
db_range("AVG", sensor, from, to)
|
36
36
|
end
|
37
37
|
|
38
|
+
def weighted_average(sensor, from, to)
|
39
|
+
total_time = 0
|
40
|
+
total_value = 0.0
|
41
|
+
previous_value = nil
|
42
|
+
start_time = nil
|
43
|
+
initialized = false
|
44
|
+
db_query "SELECT date,value FROM sensor_reads
|
45
|
+
WHERE sensor = '#{db_quote(sensor.to_s)}'
|
46
|
+
AND date >= #{from.to_s}
|
47
|
+
AND date <= #{to.to_s}" do |r|
|
48
|
+
r.each do |row|
|
49
|
+
date = row["date"].to_i
|
50
|
+
value = row["value"].to_f
|
51
|
+
if start_time
|
52
|
+
elapsed = date - start_time
|
53
|
+
total_value += elapsed * previous_value
|
54
|
+
total_time += elapsed
|
55
|
+
initialized = true
|
56
|
+
end
|
57
|
+
start_time = date
|
58
|
+
previous_value = value
|
59
|
+
end
|
60
|
+
end
|
61
|
+
initialized ? total_value / total_time : nil
|
62
|
+
end
|
63
|
+
|
38
64
|
def minimum(sensor, from, to)
|
39
65
|
db_range("MIN", sensor, from, to)
|
40
66
|
end
|
@@ -46,54 +72,51 @@ module SAAL
|
|
46
72
|
WHERE sensor = '#{db_quote(sensor.to_s)}'
|
47
73
|
AND date > '#{Time.now.utc.to_i - MAX_LAST_VAL_AGE}'
|
48
74
|
ORDER BY date DESC LIMIT 1" do |r|
|
49
|
-
|
50
|
-
|
75
|
+
row = r.first
|
76
|
+
if row
|
77
|
+
_date, value = [row["date"].to_i, row["value"].to_f]
|
78
|
+
value
|
51
79
|
else
|
52
|
-
|
53
|
-
date, value = [row[0].to_i, row[1].to_f]
|
54
|
-
return value
|
80
|
+
nil
|
55
81
|
end
|
56
82
|
end
|
57
83
|
end
|
58
84
|
|
59
85
|
def each
|
60
86
|
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]
|
87
|
+
r.each do |row|
|
88
|
+
yield [row["sensor"],row["date"].to_i, row["value"].to_f]
|
64
89
|
end
|
65
90
|
end
|
66
91
|
end
|
67
92
|
|
68
93
|
private
|
69
94
|
def db_range(function, sensor, from, to)
|
70
|
-
db_query "SELECT #{function}(value) AS
|
95
|
+
db_query "SELECT #{function}(value) AS func FROM sensor_reads
|
71
96
|
WHERE sensor = '#{db_quote(sensor.to_s)}'
|
72
97
|
AND date >= #{from.to_s}
|
73
98
|
AND date <= #{to.to_s}" do |r|
|
74
|
-
|
75
|
-
|
99
|
+
row = r.first
|
100
|
+
if row && row["func"]
|
101
|
+
row["func"].to_f
|
76
102
|
else
|
77
|
-
|
78
|
-
row[0] ? row[0].to_f : nil
|
103
|
+
nil
|
79
104
|
end
|
80
105
|
end
|
81
106
|
end
|
82
107
|
|
83
108
|
def db_quote(text)
|
84
|
-
|
109
|
+
Mysql2::Client.escape(text)
|
85
110
|
end
|
86
111
|
|
87
112
|
def db_query(query, opts={})
|
88
113
|
db = nil
|
89
114
|
begin
|
90
115
|
# connect to the MySQL server
|
91
|
-
db =
|
92
|
-
@dbopts['db'],@dbopts['port'],@dbopts['socket'],
|
93
|
-
@dbopts['flags'])
|
116
|
+
db = Mysql2::Client.new(@dbopts)
|
94
117
|
res = db.query(query)
|
95
118
|
yield res if block_given?
|
96
|
-
rescue
|
119
|
+
rescue Mysql2::Error => e
|
97
120
|
$stderr.puts "MySQL Error #{e.errno}: #{e.error}" if !(e.errno == opts[:ignoreerr])
|
98
121
|
ensure
|
99
122
|
db.close if db
|
data/lib/denkovi.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module SAAL
|
5
|
+
module Denkovi
|
6
|
+
class Outlet < SensorUnderlying
|
7
|
+
writeable!
|
8
|
+
|
9
|
+
def initialize(num, outletgroup)
|
10
|
+
@num = num
|
11
|
+
@og = outletgroup
|
12
|
+
end
|
13
|
+
|
14
|
+
def sensor_type
|
15
|
+
:onoff
|
16
|
+
end
|
17
|
+
|
18
|
+
def read(uncached = false)
|
19
|
+
{'ON' => 1.0, 'OFF' => 0.0}[@og.state(@num)]
|
20
|
+
end
|
21
|
+
|
22
|
+
def write(value)
|
23
|
+
newstate = {1.0 => 'ON', 0.0 => 'OFF'}[value]
|
24
|
+
if newstate
|
25
|
+
@og.set_state(@num,newstate)
|
26
|
+
value
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class OutletGroup
|
32
|
+
DEFAULT_TIMEOUT = 2
|
33
|
+
DEFAULT_CACHE_TIMEOUT = 5
|
34
|
+
DEFAULT_OUTLETS = {}
|
35
|
+
DEFAULT_DESCRIPTIONS = {}
|
36
|
+
|
37
|
+
attr_accessor :host, :port, :pass, :timeout, :cache_timeout
|
38
|
+
|
39
|
+
def initialize(opts={})
|
40
|
+
@host = opts[:host] || opts['host'] || 'localhost'
|
41
|
+
@port = opts[:port] || opts['port'] || 80
|
42
|
+
@pass = opts[:pass] || opts['pass'] || 'admin'
|
43
|
+
@timeout = opts[:timeout] || opts['timeout'] || DEFAULT_TIMEOUT
|
44
|
+
@cache_timeout = opts[:cache_timeout] || opts['cache_timeout'] || DEFAULT_CACHE_TIMEOUT
|
45
|
+
@outlets = opts[:outlets] || opts["outlets"] || DEFAULT_OUTLETS
|
46
|
+
@descriptions = opts[:descriptions] || opts["descriptions"] || DEFAULT_DESCRIPTIONS
|
47
|
+
@cache = nil
|
48
|
+
@cachehit = nil
|
49
|
+
@cachetime = nil
|
50
|
+
end
|
51
|
+
|
52
|
+
def state(num)
|
53
|
+
if !@cachetime or @cachetime < Time.now - @cache_timeout
|
54
|
+
@cache = do_get("/current_state.json?pw=#{@pass}")
|
55
|
+
@cachetime = Time.now
|
56
|
+
end
|
57
|
+
return nil if !@cache
|
58
|
+
json = JSON.parse(@cache.body)
|
59
|
+
num = num - 1
|
60
|
+
if json &&
|
61
|
+
json["CurrentState"] &&
|
62
|
+
json["CurrentState"]["Output"] &&
|
63
|
+
json["CurrentState"]["Output"][num] &&
|
64
|
+
json["CurrentState"]["Output"][num]["Value"]
|
65
|
+
val = json["CurrentState"]["Output"][num]["Value"]
|
66
|
+
{"1" => "ON", "0" => "OFF"}[val]
|
67
|
+
else
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def set_state(num, state)
|
73
|
+
@cachetime = nil
|
74
|
+
val = {"ON" => "1", "OFF" => "0"}[state]
|
75
|
+
if val
|
76
|
+
response = do_get("/current_state.json?pw=#{@pass}&Relay#{num}=#{val}")
|
77
|
+
response != nil
|
78
|
+
else
|
79
|
+
false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def create_sensors
|
84
|
+
sensors = {}
|
85
|
+
(1..16).each do |num|
|
86
|
+
name = @outlets[num]
|
87
|
+
if name
|
88
|
+
description = @descriptions[num] || ""
|
89
|
+
sensors[name] = [Outlet.new(num, self), description]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
sensors
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
def do_get(path)
|
97
|
+
SAAL::do_http_get(@host, @port, path, nil, nil, @timeout)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|