saal 0.2.24 → 0.3.3

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.
@@ -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