saal 0.2.2 → 0.2.11

Sign up to get free protection for your applications and to get access to all the features.
data/TODO CHANGED
@@ -1,11 +1,15 @@
1
1
  TODO
2
+ !-Index the value column of the sensor reads for minimum and maximum
3
+ - Change the filtering operations (e.g., outliercache) so that the raw value is always stored in the database
4
+ - Make the outliercache filter based on the expected sensor range (e.g. -20-50 in temperature and 800-1200 in pressure) so as to not be overly sensitive when around 0)
2
5
  - Add logging to the daemon
3
6
  ?- Change the sensor configuration to be a ruby DSL and make it a daemon config
4
7
  - Split classes into one per file with corresponding test (rails style)
5
8
  - Verify inputs on the server to make sure it never crashes
6
9
  - Remove the chart configuration options from the saal_chart script
10
+ ?- Remove Sensors and Charts and move their functionality to Sensor and Chart
7
11
  - Add an init.d file to the package (and possibly an installer script for ubuntu/debian)
8
- - Add outlier detection and removal
12
+ - Add interface that does retries for reading as well as writing (e.g., dinrelay confirm state change)
9
13
  DONE
10
14
  X- Make the server bind only to a certain interface (not applicable)
11
15
  X- Override OWNet::Connection with a mock object so that owserver is not needed
@@ -22,3 +26,5 @@ X- Make connections persistant
22
26
  - Make the date returned by GET the date of the last read (GET doesn't return
23
27
  a date now
24
28
  - Implement monthly, yearly and 10-day average charts
29
+ - Add outlier detection and removal
30
+ - Add filter support to sensor reads (e.g., altitude compensation for pressure)
data/bin/saal_chart CHANGED
@@ -1,110 +1,84 @@
1
1
  #!/usr/bin/env ruby
2
+ NUM_VALUES = 500 # Number of datapoints per series in the chart
2
3
 
3
4
  require File.dirname(__FILE__)+'/../lib/saal.rb'
4
5
 
5
6
  def usage
6
- $stderr.puts("Usage: saal_chart <day|week|4week|year|4year> <chart_file.png>")
7
+ $stderr.puts("Usage: saal_chart <chart dir>")
7
8
  end
8
9
 
9
- if ARGV.size != 2
10
+ if ARGV.size != 1
10
11
  usage
11
12
  exit (2)
12
13
  end
13
14
 
14
- @now = Time.now.utc
15
-
16
- NUM_VALUES = 500
17
- case ARGV[0]
18
- when 'day' then
19
- PERIODNAMES = (0..23).map{|i| ((@now.hour - i)%24).to_s}.reverse
20
- ALIGNMENT = :hour
21
- NUMDAYS = 1
22
- ALIGNNAMES = :center
23
- when 'week' then
24
- daynames = ["Seg","Ter","Qua","Qui","Sex","Sab","Dom"]
25
- PERIODNAMES = (1..7).map{|i| (@now.wday - i)%7}.map{|w| daynames[w]}.reverse
26
- ALIGNMENT = :day
27
- NUMDAYS = 7
28
- ALIGNNAMES = :center
29
- when '4week' then
30
- monthnames = ["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Sep","Out","Nov","Dez"]
31
- initial = @now.to_i - (@now.wday-1)*24*60*60
32
- PERIODNAMES = (0...4).map do |i|
33
- time = Time.at(initial - i*24*60*60*7)
34
- time.day.to_s+" "+ monthnames[time.month-1]
35
- end.reverse
36
- ALIGNMENT = :week
37
- NUMDAYS = 28
38
- ALIGNNAMES = :left
39
- when 'year' then
40
- monthnames = ["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Sep","Out","Nov","Dez"]
41
- initial = @now.to_i - (@now.wday-1)*24*60*60
42
- PERIODNAMES = (1..12).map{|i| (@now.month - i)%12}.map{|m| monthnames[m]}.reverse
43
- ALIGNMENT = :month
44
- NUMDAYS = 356
45
- ALIGNNAMES = :center
46
- when '4year' then
47
- PERIODNAMES = (0..3).map{|i| (@now.year - i).to_s}.reverse
48
- ALIGNMENT = :year
49
- NUMDAYS = 356*3
50
- ALIGNNAMES = :center
51
- else
52
- usage
53
- exit(3)
54
- end
55
- NUMPERIODS = PERIODNAMES.size
56
-
57
- align = {:year => [12,31,23,59,59],
58
- :month => [31,23,59,59],
59
- :day => [23,59,59],
60
- :week => [23,59,59],
61
- :hour => [59,59]}
62
-
63
- args = [@now.year, @now.month, @now.day, @now.hour, @now.min, @now.sec]
64
- args = args[0..-(align[ALIGNMENT].size+1)]
65
- args += align[ALIGNMENT]
66
- @to = Time.utc(*args).to_i
67
- @to += (6-@now.wday)*60*60*24 if ALIGNMENT == :week
68
- @from = @to - 60*60*24*NUMDAYS
69
-
70
- @sensors = SAAL::Sensors.new
71
- def create_data(sensor, min, max, constant=0)
72
- @c = SAAL::ChartData.new(@sensors.send(sensor))
73
- d = @c.get_data(@from, @to, NUM_VALUES)
74
- d = d.map{|num| num ? num+constant : num}
75
- @c.normalize_data(d,min,max)
76
- end
77
-
78
- @data = [create_data('temp_exterior', -15, 45),
79
- create_data('temp_estufa', -15, 45),
80
- create_data('hum_exterior', 0, 100),
81
- create_data('pressao', 950, 1050, 0.54*33.86)] #Convert to pressure at sea level
82
-
83
-
84
- @dataurl = @data.map {|values| values.join(",")}.join('|')
85
-
86
- r = {}
87
- case ALIGNNAMES
88
- when :center
89
- @periodnamesurl = "||"+PERIODNAMES.join('||')+"||"
90
- when :left
91
- @periodnamesurl = "|"+PERIODNAMES.join('|')+"||"
92
- r[:chxs] = "0,555555,11,-1,lt"
15
+ SENSORS = [[:temp_exterior, [-15, 45]], [:temp_estufa, [-15, 45]],
16
+ [:hum_exterior, [0,100]],
17
+ [:pressao, [950,1050]]]
18
+
19
+
20
+ SAAL::Charts.new.each do |chart|
21
+ $stderr.puts "Generating chart #{chart.name}"
22
+
23
+ pngfile = ARGV[0]+'/chart-'+chart.name.to_s+'.png'
24
+ ymlfile = ARGV[0]+'/chart-'+chart.name.to_s+'.yml'
25
+
26
+ @mins = chart.minimum
27
+ @maxs = chart.maximum
28
+ @avgs = chart.average
29
+ @minmax = {}
30
+ SENSORS.each do |s,r|
31
+ @minmax[s] = {:maximum => @maxs[s], :minimum => @mins[s], :average => @avgs[s]}
32
+ end
33
+
34
+ File.open(ymlfile, 'w').write(YAML::dump(@minmax))
35
+
36
+ def normalize_data(data, min, max, offset=0)
37
+ data.map do |i|
38
+ if i.nil?
39
+ -1.0
40
+ elsif i < min
41
+ 0.0
42
+ elsif i > max
43
+ 100.0
44
+ else
45
+ (((i-min)/(max-min).to_f)*1000).round/10.0+offset
46
+ end
47
+ end
48
+ end
49
+
50
+ @periodnames = chart.periodnames
51
+ @numperiods = @periodnames.size
52
+ @averages = chart.average(NUM_VALUES)
53
+
54
+ @data = SENSORS.map do |sensor, range|
55
+ normalize_data(@averages[sensor], *range)
56
+ end
57
+
58
+ @dataurl = @data.map {|values| values.join(",")}.join('|')
59
+
60
+ r = {}
61
+ case chart.alignlabels
62
+ when :center
63
+ @periodnamesurl = "||"+@periodnames.join('||')+"||"
64
+ when :left
65
+ @periodnamesurl = "|"+@periodnames.join('|')+"||"
66
+ r[:chxs] = "0,555555,11,-1,lt"
67
+ end
68
+ @xincr = 100.0/@numperiods.to_f*10000.truncate.to_f/10000
69
+
70
+ r[:chof] = "png"
71
+ r[:chs] = "700x300"
72
+ r[:cht] = "lc"
73
+ r[:chco] = "00ff00,ff0000,0000ff,ffff00"
74
+ r[:chxt] = "x,y,y,r"
75
+ 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"
76
+ r[:chg] = "#{@xincr},12.5,1,5"
77
+ r[:chd] = "t:#{@dataurl}"
78
+
79
+ @url = "http://chart.apis.google.com/chart?&"
80
+ @postdata = r.map{|k,v| k.to_s+"="+v}.join("&")
81
+
82
+
83
+ system "wget --quiet \"#{@url}\" --post-data=\"#{@postdata}\" -O #{pngfile}"
93
84
  end
94
- @xincr = 100.0/NUMPERIODS.to_f*10000.truncate.to_f/10000
95
-
96
- r[:chof] = "png"
97
- r[:chs] = "700x300"
98
- r[:cht] = "lc"
99
- r[:chco] = "00ff00,ff0000,0000ff,ffff00"
100
- r[:chxt] = "x,y,y,r"
101
- 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"
102
- r[:chg] = "#{@xincr},12.5,1,5"
103
- r[:chd] = "t:#{@dataurl}"
104
-
105
- @url = "http://chart.apis.google.com/chart?&"
106
- @postdata = r.map{|k,v| k.to_s+"="+v}.join("&")
107
-
108
-
109
- system "wget --quiet \"#{@url}\" --post-data=\"#{@postdata}\" -O #{ARGV[1]}"
110
-
data/bin/saal_daemon CHANGED
@@ -6,17 +6,25 @@ SENSORSCONF = "/etc/saal/sensors.yml"
6
6
 
7
7
  require File.dirname(__FILE__)+'/../lib/saal.rb'
8
8
 
9
+ def usage
10
+ $stderr.puts "Usage: saal_daemon <pidfile|--foreground>"
11
+ end
12
+
9
13
  if ARGV.size != 1
10
- $stderr.puts "Usage: saal_daemon [pidfile]"
14
+ usage
11
15
  exit 2
12
16
  else
13
- pidfile = ARGV[0]
17
+ pidfile = ARGV[0]
18
+ foreground = (ARGV[0] == '--foreground')
14
19
  d = SAAL::Daemon.new(:interval => SENSOR_INTERVAL,
15
20
  :sensorconf => SENSORSCONF,
16
- :dbconf => DBCONF)
21
+ :dbconf => DBCONF,
22
+ :foreground => foreground)
17
23
  pid = d.run
18
- File.open(pidfile, 'w') do |f|
19
- f.write(pid)
20
- f.close
24
+ if !foreground
25
+ File.open(pidfile, 'w') do |f|
26
+ f.write(pid)
27
+ f.close
28
+ end
21
29
  end
22
30
  end
data/lib/chart.rb ADDED
@@ -0,0 +1,50 @@
1
+ module SAAL
2
+ class Chart
3
+ attr_reader :name, :num, :periods, :alt, :description, :sensors, :alignlabels
4
+ def initialize(name, defs, sensors, opts={})
5
+ @name = name
6
+ @defs = defs
7
+ @alignlabels = (defs['alignlabels'] || :center).to_sym
8
+ @sensors = defs['sensors'].map{|name| sensors.send(name)}
9
+ @num = defs['last']
10
+ @periods = defs['periods']
11
+ @alt = defs['alt']
12
+ @description = defs['description']
13
+ @datarange = ChartDataRange.new(defs.merge(:now => opts[:now]))
14
+ end
15
+
16
+ def periodnames
17
+ @datarange.periodnames
18
+ end
19
+
20
+ def average(num=nil)
21
+ get_data(:average, num)
22
+ end
23
+
24
+ def minimum(num=nil)
25
+ get_data(:minimum, num)
26
+ end
27
+
28
+ def maximum(num=nil)
29
+ get_data(:maximum, num)
30
+ end
31
+
32
+ def from
33
+ @datarange.from
34
+ end
35
+ def to
36
+ @datarange.to
37
+ end
38
+
39
+ private
40
+ def get_data(method, num)
41
+ n = num || 1
42
+ h = {}
43
+ @sensors.each do |s|
44
+ data = @datarange.get_data(method,s,n)
45
+ h[s.name.to_sym] = num ? data : data[0]
46
+ end
47
+ h
48
+ end
49
+ end
50
+ end
data/lib/chart_data.rb CHANGED
@@ -1,30 +1,114 @@
1
1
  module SAAL
2
- class ChartData
3
- def initialize(sensor)
4
- @sensor = sensor
2
+ class ChartDataRange
3
+ ALIGN = {:years => [12,31,23,59,59],
4
+ :months => [31,23,59,59],
5
+ :days => [23,59,59],
6
+ :weeks => [23,59,59],
7
+ :hours => [59,59]}
8
+
9
+ NUMHOURS = {:hours => 1, :days => 24, :weeks => 24*7}
10
+ DAYNAMES = ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"]
11
+ MONTHNAMES = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]
12
+
13
+ attr_reader :num, :periods
14
+ def initialize(opts={})
15
+ last = opts[:last] || opts['last'].to_i
16
+ periods = opts[:periods] || (opts['periods'] ? opts['periods'].to_sym : nil)
17
+ @now = opts[:now] || Time.now.utc
18
+ if last && periods
19
+ @num = last
20
+ @periods = periods
21
+ calc_alignment
22
+ else
23
+ @from = opts[:from] || 0
24
+ @to = opts[:to] || @now
25
+ end
26
+ end
27
+
28
+ def from
29
+ @from.to_i
5
30
  end
6
-
7
- def get_data(from, to, num)
8
- step = (to - from).to_f/num.to_f
31
+
32
+ def to
33
+ @to.to_i
34
+ end
35
+
36
+ def get_data(method, sensor, num)
37
+ step = (@to - @from).to_i/num
38
+ t = @from - 1
9
39
  (0..num-2).map do |i|
10
- f = (from+i*step).to_i
11
- t = (from+(i+1)*step-0.5).to_i
12
- @sensor.average(f, t)
13
- end << @sensor.average((from+(num-1)*step).to_i, to)
40
+ f = t + 1
41
+ t = (f+step)
42
+ v = sensor.send(method, f.to_i, t.to_i)
43
+ end << sensor.send(method, (t+1).to_i, to.to_i)
44
+ end
45
+
46
+ def periodnames
47
+ if !@num
48
+ raise RuntimeError,
49
+ "Trying to get periodnames without a :last & :periods definition"
50
+ end
51
+
52
+ case @periods
53
+ when :hours
54
+ (0...@num).map{|i| ((@now.hour - i)%24).to_s}.reverse
55
+ when :days
56
+ (1..@num).map{|i| (@now.wday - i)%7}.map{|w| DAYNAMES[w]}.reverse
57
+ when :weeks
58
+ initial = @now - (@now.wday-1)*24*60*60
59
+ (0...@num).map do |i|
60
+ time = Time.at(initial - i*24*60*60*7)
61
+ time.day.to_s+" "+ MONTHNAMES[time.month-1]
62
+ end.reverse
63
+ when :months
64
+ (1..@num).map{|i| (@now.month - i)%12}.map{|m| MONTHNAMES[m]}.reverse
65
+ when :years
66
+ (0...@num).map{|i| (@now.year - i).to_s}.reverse
67
+ else
68
+ raise RuntimeError, "No such period type #{@periods}"
69
+ end
70
+ end
71
+
72
+ private
73
+ def calc_alignment
74
+ if [:years, :year].include? periods
75
+ # Calculate by date manipulation
76
+ from = Time.utc(@now.year - num + 1, 1, 1, 0, 0, 0)
77
+ to = Time.utc(@now.year, 12, 31, 23, 59, 59)
78
+ elsif [:months, :month].include? periods
79
+ # advance to the 1st of the next month
80
+ newm = @now.month%12 + 1
81
+ newy = @now.year + (@now.month == 12 ? 1 : 0)
82
+ to = Time.utc(newy, newm, 1, 0, 0, 0)
83
+ # Go back num months for from
84
+ from = dec_months(num, to)
85
+ # subtract 1 second from two to get the end of current month
86
+ to -= 1
87
+ else
88
+ # Calculate by elasped time
89
+ args = [@now.year, @now.month, @now.day, @now.hour, @now.min, @now.sec]
90
+ args = args[0..-(ALIGN[periods].size+1)]
91
+ args += ALIGN[periods]
92
+ to = Time.utc(*args)
93
+ to += (7-@now.wday)*60*60*24 if [:weeks,:week].include?(periods)
94
+ from = to - NUMHOURS[periods]*60*60*num+1
95
+ end
96
+ @from = from
97
+ @to = to
14
98
  end
15
-
16
- def normalize_data(data, min, max)
17
- data.map do |i|
18
- if i.nil?
19
- -1.0
20
- elsif i < min
21
- 0.0
22
- elsif i > max
23
- 100.0
24
- else
25
- v = (((i-min)/(max-min).to_f)*1000).round/10.0
26
- end
99
+
100
+ # Subtract num months from a given Time
101
+ def dec_months(num, time)
102
+ # Go back any 12 month intervals (aka years)
103
+ newy = time.year - num/12
104
+ num = num%12
105
+ # Go back the remainder months
106
+ newm = time.month - num
107
+ if newm < 1
108
+ newm = 12 - (-newm)
109
+ newy -= 1
27
110
  end
111
+ from = Time.utc(newy, newm, time.day, time.hour, time.min, time.sec)
28
112
  end
29
113
  end
30
114
  end
data/lib/charts.rb ADDED
@@ -0,0 +1,23 @@
1
+ module SAAL
2
+ class Charts
3
+ include Enumerable
4
+
5
+ def initialize(conffile=SAAL::CHARTSCONF, opts={})
6
+ @defs = YAML::load(File.new(conffile))
7
+ @sensors = opts[:sensors] || Sensors.new
8
+ @charts = {}
9
+ @defs.each do |name, defs|
10
+ @charts[name.to_sym] = Chart.new(name, defs, @sensors, opts)
11
+ end
12
+ end
13
+
14
+ # Fetch a specific chart by name
15
+ def find(name)
16
+ @charts[name.to_sym]
17
+ end
18
+
19
+ def each
20
+ @charts.each{|name, chart| yield chart}
21
+ end
22
+ end
23
+ end
data/lib/daemon.rb CHANGED
@@ -1,13 +1,18 @@
1
1
  module SAAL
2
2
  class ForkedRunner
3
3
  def self.run_as_fork(opts={})
4
- fork do
5
- if not opts[:keep_stdin]
6
- $stderr.reopen "/dev/null", "a"
7
- $stdin.reopen "/dev/null", "a"
8
- $stdout.reopen "/dev/null", "a"
9
- end
4
+ if opts[:foreground]
5
+ $stderr.puts "Running saal_daemon #{SAAL::VERSION} in foreground..."
10
6
  yield ForkedRunner.new
7
+ else
8
+ fork do
9
+ if not opts[:keep_stdin]
10
+ $stderr.reopen "/dev/null", "a"
11
+ $stdin.reopen "/dev/null", "a"
12
+ $stdout.reopen "/dev/null", "a"
13
+ end
14
+ yield ForkedRunner.new
15
+ end
11
16
  end
12
17
  end
13
18
 
data/lib/dbstore.rb CHANGED
@@ -67,7 +67,7 @@ module SAAL
67
67
  Mysql.quote(text)
68
68
  end
69
69
 
70
- def db_query(query)
70
+ def db_query(query, opts={})
71
71
  db = nil
72
72
  begin
73
73
  # connect to the MySQL server
@@ -77,7 +77,7 @@ module SAAL
77
77
  res = db.query(query)
78
78
  yield res if block_given?
79
79
  rescue Mysql::Error => e
80
- $stderr.puts "MySQL Error \#{e.errno}: \#{e.error}"
80
+ $stderr.puts "MySQL Error #{e.errno}: #{e.error}" if !(e.errno == opts[:ignoreerr])
81
81
  ensure
82
82
  db.close if db
83
83
  end
data/lib/dinrelay.rb CHANGED
@@ -10,6 +10,10 @@ module SAAL
10
10
  @og = outletgroup
11
11
  end
12
12
 
13
+ def sensor_type
14
+ :onoff
15
+ end
16
+
13
17
  def read(uncached = false)
14
18
  {'ON' => 1.0, 'OFF' => 0.0}[@og.state(@num)]
15
19
  end
@@ -33,25 +37,30 @@ module SAAL
33
37
 
34
38
  def state(num)
35
39
  response = do_get('/index.htm')
36
- return parse_index_html(response.body)[num]
40
+ return response ? parse_index_html(response.body)[num] : nil
37
41
  end
38
42
 
39
43
  def set_state(num, state)
40
44
  response = do_get("/outlet?#{num}=#{state}")
41
- response.code == "200"
45
+ response != nil
42
46
  end
43
47
 
44
48
  private
45
49
  def do_get(path)
46
- Net::HTTP.start(@host,@port) do |http|
47
- req = Net::HTTP::Get.new(path)
48
- req.basic_auth @user, @pass
49
- response = http.request(req)
50
- if response.code != "200"
51
- $stderr.puts "ERROR: Code #{response.code}"
52
- $stderr.puts response.body
50
+ begin
51
+ Net::HTTP.start(@host,@port) do |http|
52
+ req = Net::HTTP::Get.new(path)
53
+ req.basic_auth @user, @pass
54
+ response = http.request(req)
55
+ if response.code != "200"
56
+ #$stderr.puts "ERROR: Code #{response.code}"
57
+ #$stderr.puts response.body
58
+ return nil
59
+ end
60
+ return response
53
61
  end
54
- return response
62
+ rescue Exception
63
+ return nil
55
64
  end
56
65
  end
57
66
 
data/lib/outliercache.rb CHANGED
@@ -13,7 +13,7 @@ module SAAL
13
13
  # Sets how close the central values have to be for the cache to be "live"
14
14
  MAX_CACHE_DEVIATION = 0.05
15
15
  # Sets how off the read value can be from the cache median to be accepted
16
- MAX_VALUE_DEVIATION = 0.25
16
+ MAX_VALUE_DEVIATION = 0.15
17
17
 
18
18
  def initialize
19
19
  @compcache = []
data/lib/owsensor.rb CHANGED
@@ -1,12 +1,15 @@
1
1
  module SAAL
2
2
  class OWSensor < SensorUnderlying
3
- attr_reader :serial
3
+ attr_reader :serial, :sensor_type
4
4
  def initialize(defs, opts={})
5
5
  @serial = defs['serial']
6
6
  @connect_opts = {}
7
7
  @connect_opts[:server] = defs['server'] if defs['server']
8
8
  @connect_opts[:port] = defs['port'] if defs['port']
9
9
  @owconn = opts[:owconn]
10
+
11
+ basename = File.basename(@serial)
12
+ @sensor_type = basename.to_sym if ['pressure','temperature','humidity'].include?(basename)
10
13
  end
11
14
 
12
15
  def read(uncached = false)
data/lib/saal.rb CHANGED
@@ -8,8 +8,9 @@ module SAAL
8
8
  CONFDIR = "/etc/saal/"
9
9
  SENSORSCONF = CONFDIR+"sensors.yml"
10
10
  DBCONF = CONFDIR+"database.yml"
11
+ CHARTSCONF = CONFDIR+"charts.yml"
11
12
 
12
- VERSION = '0.2.2'
13
+ VERSION = '0.2.11'
13
14
  end
14
15
 
15
16
  require File.dirname(__FILE__)+'/dbstore.rb'
@@ -17,6 +18,8 @@ require File.dirname(__FILE__)+'/sensors.rb'
17
18
  require File.dirname(__FILE__)+'/sensor.rb'
18
19
  require File.dirname(__FILE__)+'/owsensor.rb'
19
20
  require File.dirname(__FILE__)+'/daemon.rb'
21
+ require File.dirname(__FILE__)+'/charts.rb'
22
+ require File.dirname(__FILE__)+'/chart.rb'
20
23
  require File.dirname(__FILE__)+'/chart_data.rb'
21
24
  require File.dirname(__FILE__)+'/outliercache.rb'
22
25
  require File.dirname(__FILE__)+'/dinrelay.rb'
data/lib/sensor.rb CHANGED
@@ -3,6 +3,7 @@ module SAAL
3
3
  end
4
4
 
5
5
  class SensorUnderlying
6
+ def sensor_type; nil; end
6
7
  def writeable?; false; end
7
8
  def self.writeable!
8
9
  define_method(:writeable?){true}
@@ -26,14 +27,24 @@ module SAAL
26
27
  @min_value = defs['min_value']
27
28
  @min_correctable = defs['min_correctable']
28
29
 
30
+ @read_offset = if defs['altitude'] && @underlying.sensor_type == :pressure
31
+ defs['altitude'].to_f/9.2
32
+ else
33
+ 0.0
34
+ end
35
+
29
36
  # Outliercache
30
37
  @outliercache = opts[:no_outliercache] ? nil : OutlierCache.new
31
- end
38
+ end
32
39
 
33
40
  def writeable?
34
41
  @underlying.writeable?
35
42
  end
36
43
 
44
+ def sensor_type
45
+ @underlying.sensor_type
46
+ end
47
+
37
48
  def read
38
49
  outlier_proof_read(false)
39
50
  end
@@ -52,22 +63,22 @@ module SAAL
52
63
 
53
64
  def average(from, to)
54
65
  return @mock_opts[:average] if @mock_opts[:average]
55
- @dbstore.average(@name, from, to)
66
+ apply_offset @dbstore.average(@name, from, to)
56
67
  end
57
68
 
58
69
  def minimum(from, to)
59
70
  return @mock_opts[:minimum] if @mock_opts[:minimum]
60
- @dbstore.minimum(@name, from, to)
71
+ apply_offset @dbstore.minimum(@name, from, to)
61
72
  end
62
73
 
63
74
  def maximum(from, to)
64
75
  return @mock_opts[:maximum] if @mock_opts[:maximum]
65
- @dbstore.maximum(@name, from, to)
76
+ apply_offset @dbstore.maximum(@name, from, to)
66
77
  end
67
78
 
68
79
  def store_value
69
80
  value = read_uncached
70
- @dbstore.write(@name, Time.now.utc.to_i, value) if value
81
+ @dbstore.write(@name, Time.now.utc.to_i, value-@read_offset) if value
71
82
  end
72
83
 
73
84
  def mock_set(opts)
@@ -87,14 +98,18 @@ module SAAL
87
98
  normalize(value)
88
99
  end
89
100
 
101
+ def apply_offset(v)
102
+ v ? v+@read_offset : v
103
+ end
104
+
90
105
  def normalize(value)
91
- if @max_value and value > @max_value
106
+ apply_offset(if @max_value and value > @max_value
92
107
  (@max_correctable and value <= @max_correctable) ? @max_value : nil
93
108
  elsif @min_value and value < @min_value
94
109
  (@min_correctable and value >= @min_correctable) ? @min_value : nil
95
110
  else
96
111
  value
97
- end
112
+ end)
98
113
  end
99
114
  end
100
115
  end
data/lib/sensors.rb CHANGED
@@ -11,8 +11,7 @@ module SAAL
11
11
  @sensors = {}
12
12
  @defs.each do |name, defs|
13
13
  self.class.sensors_from_defs(@dbstore, name, defs).each{|s| @sensors[s.name] = s}
14
- end
15
-
14
+ end
16
15
  end
17
16
 
18
17
  # Implements the get methods to fetch a specific sensor
@@ -36,7 +35,9 @@ module SAAL
36
35
  elsif defs['dinrelay']
37
36
  og = DINRelay::OutletGroup.new(defs['dinrelay'])
38
37
  outlet_names = defs['dinrelay']['outlets'] || []
38
+ outlet_descriptions = defs['dinrelay']['descriptions'] || []
39
39
  return outlet_names.map do |num, oname|
40
+ defs.merge!('name' => outlet_descriptions[num])
40
41
  Sensor.new(dbstore, oname, DINRelay::Outlet.new(num.to_i, og), defs, opts)
41
42
  end
42
43
  else
data/saal.gemspec CHANGED
@@ -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.2'
10
- s.date = '2010-12-29'
9
+ s.version = '0.2.11'
10
+ s.date = '2011-05-26'
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
@@ -46,7 +46,9 @@ EOF
46
46
  bin/saal_dump_database
47
47
  bin/saal_import_mysql
48
48
  bin/saal_readall
49
+ lib/chart.rb
49
50
  lib/chart_data.rb
51
+ lib/charts.rb
50
52
  lib/daemon.rb
51
53
  lib/dbstore.rb
52
54
  lib/dinrelay.rb
@@ -57,6 +59,8 @@ EOF
57
59
  lib/sensors.rb
58
60
  saal.gemspec
59
61
  test/chart_data_test.rb
62
+ test/chart_test.rb
63
+ test/charts_test.rb
60
64
  test/daemon_test.rb
61
65
  test/dbstore_test.rb
62
66
  test/dinrelay.html.erb
@@ -65,6 +69,7 @@ EOF
65
69
  test/outliercache_test.rb
66
70
  test/sensor_test.rb
67
71
  test/sensors_test.rb
72
+ test/test_charts.yml
68
73
  test/test_db.yml
69
74
  test/test_dinrelay_sensors.yml
70
75
  test/test_helper.rb
@@ -18,22 +18,115 @@ class MockSensor
18
18
  end
19
19
  end
20
20
 
21
- class TestChartData < Test::Unit::TestCase
22
- def test_get_data
21
+ class TestChartData < Test::Unit::TestCase
22
+ def test_basic_range
23
23
  sensor = MockSensor.new
24
- c = SAAL::ChartData.new(sensor)
25
- assert_equal MOCK_AVERAGES, c.get_data(0, 1000, 5)
26
- assert_equal([[0,199],[200,399],[400,599],[600,799],[800,1000]],
24
+ range = SAAL::ChartDataRange.new(:from => 1, :to => 1000)
25
+ assert_equal MOCK_AVERAGES, range.get_data(:average, sensor, 5)
26
+ assert_equal([[1,200],[201,400],[401,600],[601,800],[801,1000]],
27
27
  sensor.asked_averages)
28
28
  end
29
-
30
- def test_normalize_data
29
+
30
+ def test_interval_range
31
+ sensor = MockSensor.new
32
+ now = Time.utc(2010, 12, 30, 15, 38, 19)
33
+ ranges = [[1293638400,1293655679],[1293655680,1293672959],
34
+ [1293672960,1293690239],[1293690240,1293707519],
35
+ [1293707520,1293724799]]
36
+ range = SAAL::ChartDataRange.new(:last => 24, :periods => :hours, :now => now)
37
+ assert_equal MOCK_AVERAGES, range.get_data(:average, sensor, 5)
38
+ assert_equal ranges, sensor.asked_averages
39
+ end
40
+
41
+ def test_correct_time_use
31
42
  sensor = MockSensor.new
32
- c = SAAL::ChartData.new(sensor)
33
- d = c.get_data(0, 1000, 5)
34
- assert_equal NORMALIZED_MOCK_AVERAGES,
35
- c.normalize_data(d, MOCK_MIN, MOCK_MAX)
36
- assert_equal([[0,199],[200,399],[400,599],[600,799],[800,1000]],
37
- sensor.asked_averages)
43
+ range = SAAL::ChartDataRange.new(:last => 24, :periods => :hours)
44
+ now = Time.now.utc
45
+ to = Time.utc(now.year,now.month,now.day,now.hour,59,59).to_i
46
+ from = to - 24*60*60 + 1
47
+ assert_equal MOCK_AVERAGES[0..0], range.get_data(:average, sensor, 1)
48
+ assert_equal [[from,to]], sensor.asked_averages
49
+ end
50
+
51
+ # Test all the alignment functions underlying :last, :periods
52
+ def self.assert_alignment_interval(num,periods,from,to, periodnames=nil,
53
+ now = nil, extra=nil)
54
+ define_method("test_alignment_#{num}#{periods}#{extra.to_s}") do
55
+ now = now || Time.utc(2010, 12, 30, 15, 38, 19)
56
+ o = SAAL::ChartDataRange.new(:last => num, :periods => periods, :now => now)
57
+ assert_equal [from.to_i, to.to_i], [o.from, o.to],
58
+ "Expecting #{from.utc} - #{to.utc}\n"+
59
+ "Got #{Time.at(o.from).utc} - #{Time.at(o.to).utc}"
60
+ assert_equal periodnames, o.periodnames if periodnames
61
+ end
62
+ end
63
+ assert_alignment_interval(24, :hours, Time.utc(2010, 12, 29, 16, 0, 0),
64
+ Time.utc(2010, 12, 30, 15, 59, 59),
65
+ (16..23).map{|s| s.to_s}+(0..15).map{|s| s.to_s})
66
+
67
+ assert_alignment_interval(22, :hours, Time.utc(2010, 12, 29, 18, 0, 0),
68
+ Time.utc(2010, 12, 30, 15, 59, 59),
69
+ (18..23).map{|s| s.to_s}+(0..15).map{|s| s.to_s})
70
+
71
+ assert_alignment_interval(1, :days, Time.utc(2010, 12, 30, 0, 0, 0),
72
+ Time.utc(2010, 12, 30, 23, 59, 59),
73
+ ["Thu"])
74
+
75
+ assert_alignment_interval(12, :hours, Time.utc(2010, 12, 30, 4, 0, 0),
76
+ Time.utc(2010, 12, 30, 15, 59, 59),
77
+ (4..15).map{|s| s.to_s})
78
+
79
+ assert_alignment_interval(1, :weeks,Time.utc(2010, 12, 27, 0, 0, 0),
80
+ Time.utc(2011, 1, 2, 23, 59, 59),
81
+ ["27 Dec"])
82
+
83
+ assert_alignment_interval(2, :weeks,Time.utc(2010, 12, 20, 0, 0, 0),
84
+ Time.utc(2011, 1, 2, 23, 59, 59),
85
+ ["20 Dec","27 Dec"])
86
+
87
+ assert_alignment_interval(1, :years, Time.utc(2010, 1, 1, 0, 0, 0),
88
+ Time.utc(2010, 12, 31, 23, 59, 59),
89
+ ["2010"])
90
+
91
+ assert_alignment_interval(2, :years, Time.utc(2009, 1, 1, 0, 0, 0),
92
+ Time.utc(2010, 12, 31, 23, 59, 59),
93
+ ["2009","2010"])
94
+
95
+ assert_alignment_interval(1, :months, Time.utc(2010, 12, 1, 0, 0, 0),
96
+ Time.utc(2010, 12, 31, 23, 59, 59),
97
+ ["Dec"])
98
+ assert_alignment_interval(1, :months, Time.utc(2010, 4, 1, 0, 0, 0),
99
+ Time.utc(2010, 4, 30, 23, 59, 59),
100
+ ["Apr"], Time.utc(2010, 4, 30, 12, 50, 30),
101
+ "_30day_month")
102
+
103
+ assert_alignment_interval(12, :months, Time.utc(2010, 1, 1, 0, 0, 0),
104
+ Time.utc(2010, 12, 31, 23, 59, 59),
105
+ ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug",
106
+ "Sep","Oct","Nov","Dec"])
107
+
108
+ assert_alignment_interval(13, :months, Time.utc(2009, 12, 1, 0, 0, 0),
109
+ Time.utc(2010, 12, 31, 23, 59, 59))
110
+ assert_alignment_interval(24, :months, Time.utc(2009, 1, 1, 0, 0, 0),
111
+ Time.utc(2010, 12, 31, 23, 59, 59))
112
+ assert_alignment_interval(12, :months, Time.utc(2010, 2, 1, 0, 0, 0),
113
+ Time.utc(2011, 1, 31, 23, 59, 59),
114
+ ["Feb","Mar","Apr","May","Jun","Jul","Aug",
115
+ "Sep","Oct","Nov","Dec","Jan"],
116
+ Time.utc(2011, 1, 1, 14, 15, 10),
117
+ "_midyear")
118
+
119
+ def self.assert_dec_months(num, from, to)
120
+ define_method("test_dec_#{num}months_from#{from.join('-')}_to#{to.join('-')}") do
121
+ o = SAAL::ChartDataRange.new
122
+ assert_equal Time.utc(to[0], to[1], 1, 0, 0, 0),
123
+ o.send(:dec_months, num, Time.utc(from[0],from[1],1,0,0,0))
124
+ end
38
125
  end
126
+
127
+ assert_dec_months 2, [2010,12],[2010,10]
128
+ assert_dec_months 12, [2011,1],[2010,1]
129
+ assert_dec_months 24, [2011,1],[2009,1]
130
+ assert_dec_months 2, [2011,1],[2010,11]
131
+ assert_dec_months 14, [2011,1],[2009,11]
39
132
  end
@@ -0,0 +1,55 @@
1
+ require File.dirname(__FILE__)+'/test_helper.rb'
2
+
3
+ class TestChart < Test::Unit::TestCase
4
+ def setup
5
+ @defs = YAML::load File.new(TEST_CHARTS_FILE)
6
+ sensors = SAAL::Sensors.new(TEST_SENSORS_FILE, TEST_DBCONF)
7
+ @charts = SAAL::Charts.new(TEST_CHARTS_FILE,
8
+ :sensors => sensors,
9
+ :now => Time.utc(2010, 12, 30, 15, 38, 19))
10
+ end
11
+
12
+ def test_alignlabels
13
+ assert_equal :center, @charts.find('week').alignlabels
14
+ assert_equal :left, @charts.find('4week').alignlabels
15
+ end
16
+
17
+ def test_average
18
+ name = 'week'
19
+ defs = @defs[name]
20
+ chart = @charts.find(name)
21
+ assert_equal ['Fri','Sat','Sun','Mon','Tue','Wed','Thu'], chart.periodnames
22
+ chart.sensors.each {|s| s.mock_set(:average => 1)}
23
+ assert_equal({:fake_temp => [1], :non_existant => [1]}, chart.average(1))
24
+ end
25
+
26
+ def test_from_to
27
+ chart = @charts.find('day')
28
+ assert_equal Time.utc(2010, 12, 29, 16, 0, 0).to_i, chart.from
29
+ assert_equal Time.utc(2010, 12, 30, 15, 59, 59).to_i, chart.to
30
+ end
31
+
32
+ def test_min_max_avg_1arity
33
+ name = 'week'
34
+ defs = @defs[name]
35
+ chart = @charts.find(name)
36
+ assert_equal ['Fri','Sat','Sun','Mon','Tue','Wed','Thu'], chart.periodnames
37
+ v = {:minimum => 1.0, :maximum => 2.0, :average => 1.5}
38
+ [:minimum,:maximum,:average].each do |method|
39
+ chart.sensors.each {|s| s.mock_set(method => v[method])}
40
+ assert_equal({:fake_temp => [v[method]], :non_existant => [v[method]]}, chart.send(method,1))
41
+ end
42
+ end
43
+
44
+ def test_min_max_0arity
45
+ name = 'week'
46
+ defs = @defs[name]
47
+ chart = @charts.find(name)
48
+ assert_equal ['Fri','Sat','Sun','Mon','Tue','Wed','Thu'], chart.periodnames
49
+ v = {:minimum => 1.0, :maximum => 2.0, :average => 1.5}
50
+ [:minimum,:maximum,:average].each do |method|
51
+ chart.sensors.each {|s| s.mock_set(method => v[method])}
52
+ assert_equal({:fake_temp => v[method], :non_existant => v[method]}, chart.send(method))
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,31 @@
1
+ require File.dirname(__FILE__)+'/test_helper.rb'
2
+
3
+ class TestCharts < Test::Unit::TestCase
4
+ def setup
5
+ @defs = YAML::load File.new(TEST_CHARTS_FILE)
6
+ sensors = SAAL::Sensors.new(TEST_SENSORS_FILE, TEST_DBCONF)
7
+ @charts = SAAL::Charts.new(TEST_CHARTS_FILE, :sensors => sensors)
8
+ end
9
+
10
+ def test_load
11
+ @defs.each do |name, defs|
12
+ chart = @charts.find(name)
13
+ assert_instance_of SAAL::Chart, chart
14
+ assert_equal name, chart.name
15
+ end
16
+ end
17
+
18
+ def test_each
19
+ i = 0
20
+ @charts.each do |chart|
21
+ defs = @defs[chart.name]
22
+ assert_equal defs['last'], chart.num
23
+ assert_equal defs['periods'], chart.periods
24
+ assert_equal defs['description'], chart.description
25
+ assert_equal defs['alt'], chart.alt
26
+ assert_equal defs['sensors'], chart.sensors.map{|s| s.name.to_s}
27
+ i += 1
28
+ end
29
+ assert_equal @defs.size, i, "Charts.each did not iterate correctly"
30
+ end
31
+ end
@@ -14,6 +14,7 @@ class TestDINRelay < Test::Unit::TestCase
14
14
  @html = opts[:html]
15
15
  @user = opts[:user]
16
16
  @pass = opts[:pass]
17
+ @status = opts[:status] || 200
17
18
  @feedback = opts[:feedback] || {}
18
19
  end
19
20
  def do_GET(req, res)
@@ -22,7 +23,8 @@ class TestDINRelay < Test::Unit::TestCase
22
23
  user == @user && pass == @pass
23
24
  }
24
25
  res.body = @html
25
- res['Content-Type'] = "text/xml"
26
+ res.status = @status
27
+ res['Content-Type'] = "text/html"
26
28
  end
27
29
  end
28
30
 
@@ -84,6 +86,13 @@ class TestDINRelay < Test::Unit::TestCase
84
86
  def test_enumerate_sensors
85
87
  sensors = SAAL::Sensors.new(TEST_SENSORS_DINRELAY_FILE, TEST_DBCONF)
86
88
  assert_equal((1..8).map{|i| "name#{i}"}, sensors.map{|s| s.name}.sort)
89
+ assert_equal((1..8).map{|i| "description#{i}"}, sensors.map{|s| s.description}.sort)
90
+ end
91
+
92
+ def test_sensor_type
93
+ SAAL::Sensors.new(TEST_SENSORS_DINRELAY_FILE, TEST_DBCONF).each do |s|
94
+ assert_equal :onoff, s.sensor_type
95
+ end
87
96
  end
88
97
 
89
98
  def test_read_sensors
@@ -109,4 +118,20 @@ class TestDINRelay < Test::Unit::TestCase
109
118
  end
110
119
  end
111
120
  end
121
+
122
+ def test_failed_connection
123
+ @vals.each do |num, state|
124
+ assert_equal nil, @og.state(num)
125
+ assert !@og.set_state(num,"ON"), "State change working without a server?!"
126
+ end
127
+ end
128
+
129
+ def test_failed_request
130
+ with_webrick(:html=>create_index_html(@vals),:status=>404) do |feedback|
131
+ @vals.each do |num, state|
132
+ assert_equal nil, @og.state(num)
133
+ assert !@og.set_state(num,"ON"), "State change working without a server?!"
134
+ end
135
+ end
136
+ end
112
137
  end
data/test/sensor_test.rb CHANGED
@@ -11,10 +11,19 @@ class MockConnection
11
11
  end
12
12
 
13
13
  class MockDBStore
14
- attr_accessor :value
14
+ attr_accessor :value, :stored_value
15
15
  def average(sensor, from, to)
16
16
  @value
17
17
  end
18
+ def minimum(sensor, from, to)
19
+ @value
20
+ end
21
+ def maximum(sensor, from, to)
22
+ @value
23
+ end
24
+ def write(sensor,date,value)
25
+ @stored_value = value
26
+ end
18
27
  end
19
28
 
20
29
  class TestSensor < Test::Unit::TestCase
@@ -92,6 +101,33 @@ class TestSensor < Test::Unit::TestCase
92
101
  assert_equal [0]*20+[1000], (1..21).map{@fake3.read_uncached}
93
102
  end
94
103
 
104
+ def test_eliminate_outliers
105
+ correctread = 994.422
106
+ fakeread = 817.309
107
+ @conn.values = [correctread]*20 + [fakeread,correctread]
108
+ assert_equal [correctread]*21, (1..21).map{@fake3.read}
109
+ @conn.values = [correctread]*20 + [fakeread,correctread]
110
+ assert_equal [correctread]*21, (1..21).map{@fake3.read_uncached}
111
+ end
112
+
113
+ def test_sealevel_correction
114
+ sensor = fake_sensor('pressure')
115
+ @conn.value = @dbstore.value = 1000
116
+ corrected = 1000+@defs['pressure']['altitude'].to_f/9.2
117
+ assert_equal corrected, sensor.read
118
+ sensor.store_value
119
+ assert_equal 1000, @dbstore.stored_value
120
+ assert_equal corrected, sensor.minimum(0,100)
121
+ assert_equal corrected, sensor.maximum(0,100)
122
+ assert_equal corrected, sensor.average(0,100)
123
+ end
124
+
125
+ def test_sensor_type
126
+ [:pressure, :humidity, :temperature].each do |type|
127
+ assert_equal type, fake_sensor(type.to_s).sensor_type
128
+ end
129
+ end
130
+
95
131
  def test_mocked
96
132
  @mockable = fake_sensor('fake3')
97
133
  @conn.value = 1.0
@@ -0,0 +1,27 @@
1
+ day:
2
+ sensors: [fake_temp, non_existant]
3
+ last: 24
4
+ periods: hours
5
+ alt: Some ALT text
6
+ description: some description of the chart
7
+
8
+ week:
9
+ sensors: [fake_temp, non_existant]
10
+ last: 7
11
+ periods: days
12
+
13
+ 4week:
14
+ sensors: [fake_temp, non_existant]
15
+ last: 4
16
+ periods: weeks
17
+ alignlabels: left
18
+
19
+ year:
20
+ sensors: [fake_temp, non_existant]
21
+ last: 12
22
+ periods: months
23
+
24
+ 4year:
25
+ sensors: [fake_temp, non_existant]
26
+ last: 4
27
+ periods: years
@@ -13,4 +13,12 @@ group1:
13
13
  6: name6
14
14
  7: name7
15
15
  8: name8
16
-
16
+ descriptions:
17
+ 1: description1
18
+ 2: description2
19
+ 3: description3
20
+ 4: description4
21
+ 5: description5
22
+ 6: description6
23
+ 7: description7
24
+ 8: description8
data/test/test_helper.rb CHANGED
@@ -8,6 +8,7 @@ class Test::Unit::TestCase
8
8
  TEST_SENSORS_DINRELAY_FILE = File.dirname(__FILE__)+'/test_dinrelay_sensors.yml'
9
9
  TEST_SENSOR_CLEANUPS_FILE = File.dirname(__FILE__)+'/test_sensor_cleanups.yml'
10
10
  TEST_NONEXIST_SENSOR_FILE = File.dirname(__FILE__)+'/nonexistant_sensor.yml'
11
+ TEST_CHARTS_FILE = File.dirname(__FILE__)+'/test_charts.yml'
11
12
  TEST_DBCONF = File.dirname(__FILE__)+'/test_db.yml'
12
13
  TEST_DBOPTS = YAML::load(File.new(TEST_DBCONF))
13
14
 
@@ -19,3 +19,19 @@ fake3:
19
19
  onewire:
20
20
  serial: /10.4AEC29CDBAAB/temperature
21
21
 
22
+ pressure:
23
+ name: "A fake pressure sensor"
24
+ altitude: 200
25
+ onewire:
26
+ serial: /10.4AEC29CDBAAB/pressure
27
+ temperature:
28
+ name: "A fake pressure sensor"
29
+ onewire:
30
+ serial: /10.4AEC29CDBAAB/temperature
31
+ humidity:
32
+ name: "A fake pressure sensor"
33
+ onewire:
34
+ serial: /10.4AEC29CDBAAB/humidity
35
+
36
+
37
+
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: saal
3
3
  version: !ruby/object:Gem::Version
4
- hash: 19
4
+ hash: 1
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 2
9
- - 2
10
- version: 0.2.2
9
+ - 11
10
+ version: 0.2.11
11
11
  platform: ruby
12
12
  authors:
13
13
  - "Pedro C\xC3\xB4rte-Real"
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-12-29 00:00:00 +00:00
18
+ date: 2011-05-26 00:00:00 -07:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -94,7 +94,9 @@ files:
94
94
  - bin/saal_dump_database
95
95
  - bin/saal_import_mysql
96
96
  - bin/saal_readall
97
+ - lib/chart.rb
97
98
  - lib/chart_data.rb
99
+ - lib/charts.rb
98
100
  - lib/daemon.rb
99
101
  - lib/dbstore.rb
100
102
  - lib/dinrelay.rb
@@ -105,6 +107,8 @@ files:
105
107
  - lib/sensors.rb
106
108
  - saal.gemspec
107
109
  - test/chart_data_test.rb
110
+ - test/chart_test.rb
111
+ - test/charts_test.rb
108
112
  - test/daemon_test.rb
109
113
  - test/dbstore_test.rb
110
114
  - test/dinrelay.html.erb
@@ -113,6 +117,7 @@ files:
113
117
  - test/outliercache_test.rb
114
118
  - test/sensor_test.rb
115
119
  - test/sensors_test.rb
120
+ - test/test_charts.yml
116
121
  - test/test_db.yml
117
122
  - test/test_dinrelay_sensors.yml
118
123
  - test/test_helper.rb
@@ -157,6 +162,8 @@ specification_version: 2
157
162
  summary: Thin abstraction layer for interfacing and recording sensors (currently onewire) and actuators (currently dinrelay)
158
163
  test_files:
159
164
  - test/chart_data_test.rb
165
+ - test/chart_test.rb
166
+ - test/charts_test.rb
160
167
  - test/daemon_test.rb
161
168
  - test/dbstore_test.rb
162
169
  - test/dinrelay_test.rb