saal 0.2.25 → 0.3.4

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 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
@@ -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{|name| sensors.send(name)}
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
@@ -39,7 +39,7 @@ module SAAL
39
39
  (0..num-2).map do |i|
40
40
  f = t + 1
41
41
  t = (f+step)
42
- v = sensor.send(method, f.to_i, t.to_i)
42
+ _v = sensor.send(method, f.to_i, t.to_i)
43
43
  end << sensor.send(method, (t+1).to_i, to.to_i)
44
44
  end
45
45
 
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
- if r.num_rows == 0
50
- return nil
75
+ row = r.first
76
+ if row
77
+ _date, value = [row["date"].to_i, row["value"].to_f]
78
+ value
51
79
  else
52
- row = r.fetch_row
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.num_rows.times do
62
- row = r.fetch_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 average FROM sensor_reads
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
- if r.num_rows == 0
75
- nil
99
+ row = r.first
100
+ if row && row["func"]
101
+ row["func"].to_f
76
102
  else
77
- row = r.fetch_row
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
- Mysql.quote(text)
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 = Mysql.new(@dbopts['host'],@dbopts['user'],@dbopts['pass'],
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 Mysql::Error => e
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