saal 0.2.24 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 90aa53b1093427163af28bee630532e79ccefea47de44463641c3e387f038640
4
+ data.tar.gz: 41fca314cb32d91ca10bfb1982cf7e60a6b584c3c521b8ddcd148b18f465fd86
5
+ SHA512:
6
+ metadata.gz: ff26de3ff3cca4750a365b9672a82165d449f7c11fb3bd57d7f3f27758636a521d766ae99dc84b50cd30e940453eb30826ad485e44f9e50d723bb5625cd07915
7
+ data.tar.gz: f93e18eacc3a2eb6555c95b9ea91f74506a39af91c6083ab6dbb3acc1ef2d83387db9ed59a573afcb7bbd1354cfdf7af51ec11122d9f48f287baf273c235debf
@@ -16,7 +16,11 @@ if ARGV.size != 1
16
16
  exit (2)
17
17
  end
18
18
 
19
- SENSOR_RANGES = {:temperature=>[-15, 45], :humidity=>[0,100], :pressure=>[950,1050]}
19
+ MAX_RANGES = {:temperature=>[-10,70],
20
+ :humidity=>[0,110],
21
+ :pressure=>[900,1200]}
22
+
23
+ TYPES = {:temperature => "ºC", :humidity => "%", :pressure => "hPa"}
20
24
 
21
25
 
22
26
  SAAL::Charts.new.each do |chart|
@@ -57,9 +61,35 @@ SAAL::Charts.new.each do |chart|
57
61
  NUM_VALUES_SMALL
58
62
 
59
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
60
80
 
81
+ # Then clip those intervals to MAX_RANGES
61
82
  @data = chart.sensors.map do |sensor|
62
- normalize_data(@averages[sensor.name.to_sym], *(SENSOR_RANGES[sensor.sensor_type]))
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])
63
93
  end
64
94
 
65
95
  @dataurl = @data.map {|values| values.join(",")}.join('|')
@@ -74,12 +104,27 @@ SAAL::Charts.new.each do |chart|
74
104
  end
75
105
  @xincr = 100.0/@numperiods.to_f*10000.truncate.to_f/10000
76
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
+
77
122
  r[:chof] = "png"
78
123
  r[:chs] = "700x300"
79
124
  r[:cht] = "lc"
80
125
  r[:chco] = "00ff00,ff0000,0000ff,ff9933,800080"
81
- r[:chxt] = "x,y,y,r"
82
- 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"
126
+ r[:chxt] = "x,#{@axisset}"
127
+ r[:chxl] = "0:#{@periodnamesurl}#{@axes}"
83
128
  r[:chg] = "#{@xincr},12.5,1,5"
84
129
  r[:chd] = "t:#{@dataurl}"
85
130
 
@@ -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,144 @@
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
+ :types => ["w_now", "last_report_date", "w_max"],
134
+ )
135
+ envoy.set_all_inverters!
136
+ inverters = envoy.create_sensors
137
+ puts " Found #{envoy.inverters.size} inverters"
138
+ envoy.inverters.each do |serial|
139
+ puts " INVERTER: #{serial} \
140
+ date:#{l(inverters,"inverters_#{serial}_last_report_date")} \
141
+ lastWatts:#{l(inverters,"inverters_#{serial}_w_now")} \
142
+ maxWatts:#{l(inverters,"inverters_#{serial}_w_max")} \
143
+ "
144
+ end
@@ -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']
@@ -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
 
@@ -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
- if r.num_rows == 0
50
- return nil
49
+ row = r.first
50
+ if row
51
+ _date, value = [row["date"].to_i, row["value"].to_f]
52
+ value
51
53
  else
52
- row = r.fetch_row
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.num_rows.times do
62
- row = r.fetch_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 average FROM sensor_reads
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
- if r.num_rows == 0
75
- nil
73
+ row = r.first
74
+ if row && row["func"]
75
+ row["func"].to_f
76
76
  else
77
- row = r.fetch_row
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
- Mysql.quote(text)
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 = Mysql.new(@dbopts['host'],@dbopts['user'],@dbopts['pass'],
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 Mysql::Error => e
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
@@ -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 = 60
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