saal 0.2.22 → 0.3.1

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: 62d7f9b51083356448d50bcb5ad66c729d84a38e152f85470ca4fa5f9f5fed15
4
+ data.tar.gz: 73f51e4736d14ee249531535577a8ff0568e0c19fb3115a4cf76bb0801d8e504
5
+ SHA512:
6
+ metadata.gz: 349eed054b1fd37de656840840b92e1be279e77fe677b92cc47864fe96079f88e6dc784e608bf5a7843b08cc0025b1f141d1bf88be1cef553e18f104cc0c3d74
7
+ data.tar.gz: e07a22761c8f4f6c01d631abb29b01c63d6888c449fd67762e558ddd7693cd1bdfdf5b8bd7cdc35575006c6cb2fc9cbef49329bf08b754d5101d5dfc20d3421d
@@ -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
- 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"}
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
- 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])
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,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"
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
@@ -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
@@ -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
@@ -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
- begin
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"]')
@@ -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
@@ -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
@@ -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){|sum,el| sum+el}
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|
@@ -1,5 +1,5 @@
1
1
  require 'yaml'
2
- require "mysql"
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.2.22'
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
 
@@ -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
- raise UnknownSensorType, "Couldn't figure out a valid sensor type "
45
- "from the configuration for #{name}"
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
@@ -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.2.22'
10
- s.date = '2013-05-16'
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.add_dependency('ownet', [">= 0.2.1"])
32
- s.add_dependency('nokogiri')
33
- s.add_dependency('mysql')
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
@@ -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
- "_changing_timezone_test","Europe/Lisbon")
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),
@@ -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}
@@ -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.num_rows > 0, "No sensor reads in DB"
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.num_rows == 0
41
+ assert res.count == 0
42
42
  end
43
43
  end
44
44
  end
@@ -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.num_rows
13
- assert_equal ["test_sensor", test_time.to_s, test_value.to_s], res.fetch_row
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
 
@@ -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
- sensors = SAAL::Sensors.new(@test_sensors_dinrelay_file, TEST_DBCONF)
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)
@@ -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.num_rows
71
- row = res.fetch_row
72
- assert_equal "fake_temp", row[0]
73
- assert_in_delta Time.now.utc.to_i, row[1].to_i, 100
74
- assert_instance_of Float, row[2].to_f
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
@@ -1,4 +1,4 @@
1
1
  host: localhost
2
- user: sensor_reads
3
- pass: password
4
- db: sensor_reads_test
2
+ username: sensor_reads
3
+ password: password
4
+ database: sensor_reads_test
@@ -29,8 +29,7 @@ class Test::Unit::TestCase
29
29
  end
30
30
 
31
31
  def db_test_query(query)
32
- db = Mysql.new(TEST_DBOPTS['host'],TEST_DBOPTS['user'],
33
- TEST_DBOPTS['pass'],TEST_DBOPTS['db'])
32
+ db = Mysql2::Client.new(TEST_DBOPTS)
34
33
  res = db.query(query)
35
34
  yield res
36
35
  db.close
@@ -1,7 +1,7 @@
1
1
  fake_temp:
2
2
  name: "A fake temperature sensor"
3
3
  onewire:
4
- serial: /10.4AEC29CDBAAB/temperature
4
+ serial: /10.67C6697351FF/temperature
5
5
 
6
6
  non_existant:
7
7
  name: "A non-existant sensor"
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.2.22
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: 2013-05-16 00:00:00.000000000 Z
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: &72850210 !ruby/object:Gem::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.1
19
+ version: '0.2'
22
20
  type: :runtime
23
21
  prerelease: false
24
- version_requirements: *72850210
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: &72849690 !ruby/object:Gem::Requirement
28
- none: false
43
+ requirement: !ruby/object:Gem::Requirement
29
44
  requirements:
30
- - - ! '>='
45
+ - - "~>"
31
46
  - !ruby/object:Gem::Version
32
- version: '0'
47
+ version: '1.8'
33
48
  type: :runtime
34
49
  prerelease: false
35
- version_requirements: *72849690
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: mysql
38
- requirement: &72848430 !ruby/object:Gem::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: '0'
61
+ version: '1.4'
44
62
  type: :runtime
45
63
  prerelease: false
46
- version_requirements: *72848430
47
- description: ! "A daemon and libraries to create an abstraction layer that interfaces
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
- post_install_message:
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
- rubyforge_project:
133
- rubygems_version: 1.8.11
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)
@@ -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
@@ -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