saal 0.2.14 → 0.2.21

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -3,9 +3,10 @@
3
3
  require 'rubygems'
4
4
  require 'rake'
5
5
  require 'date'
6
+ require 'rdoc'
6
7
  require 'rcov/rcovtask'
7
8
  require 'rake/testtask'
8
- require 'rake/rdoctask'
9
+ require 'rdoc/task'
9
10
 
10
11
  #############################################################################
11
12
  #
data/TODO CHANGED
@@ -4,11 +4,11 @@ TODO
4
4
  !-Index the value column of the sensor reads for minimum and maximum
5
5
  - Change the filtering operations (e.g., outliercache) so that the raw value is always stored in the database
6
6
  - 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)
7
+ - Alternatively try using a different method to integrate values (Kalman filter?)
7
8
  - Add logging to the daemon
8
9
  ?- Change the sensor configuration to be a ruby DSL and make it a daemon config
9
10
  - Split classes into one per file with corresponding test (rails style)
10
11
  - Verify inputs on the server to make sure it never crashes
11
- - Remove the chart configuration options from the saal_chart script
12
12
  ?- Remove Sensors and Charts and move their functionality to Sensor and Chart
13
13
  - Add an init.d file to the package (and possibly an installer script for ubuntu/debian)
14
14
  - Add interface that does retries for reading as well as writing (e.g., dinrelay confirm state change)
data/bin/saal_chart CHANGED
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
- NUM_VALUES = 500 # Number of datapoints per series in the chart
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)
3
6
 
4
7
  require File.dirname(__FILE__)+'/../lib/saal.rb'
5
8
 
@@ -48,7 +51,11 @@ SAAL::Charts.new.each do |chart|
48
51
 
49
52
  @periodnames = chart.periodnames
50
53
  @numperiods = @periodnames.size
51
- @averages = chart.average(NUM_VALUES)
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)
52
59
 
53
60
  @data = chart.sensors.map do |sensor|
54
61
  normalize_data(@averages[sensor.name.to_sym], *(SENSOR_RANGES[sensor.sensor_type]))
@@ -69,7 +76,7 @@ SAAL::Charts.new.each do |chart|
69
76
  r[:chof] = "png"
70
77
  r[:chs] = "700x300"
71
78
  r[:cht] = "lc"
72
- r[:chco] = "00ff00,ff0000,0000ff,ffff00"
79
+ r[:chco] = "00ff00,ff0000,0000ff,ff9933,800080"
73
80
  r[:chxt] = "x,y,y,r"
74
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"
75
82
  r[:chg] = "#{@xincr},12.5,1,5"
data/bin/saal_chart~ ADDED
@@ -0,0 +1,90 @@
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
data/lib/chart_data.rb CHANGED
@@ -51,7 +51,7 @@ module SAAL
51
51
 
52
52
  case @periods
53
53
  when :hours
54
- (0...@num).map{|i| ((@now.hour - i)%24).to_s}.reverse
54
+ (0...@num).map{|i| ((@now.getlocal - i*3600).hour).to_s}.reverse
55
55
  when :days
56
56
  (1..@num).map{|i| (@now.wday - i)%7}.map{|w| DAYNAMES[w]}.reverse
57
57
  when :weeks
@@ -82,7 +82,7 @@ module SAAL
82
82
  to = Time.utc(newy, newm, 1, 0, 0, 0)
83
83
  # Go back num months for from
84
84
  from = dec_months(num, to)
85
- # subtract 1 second from two to get the end of current month
85
+ # subtract 1 second from to to get the end of current month
86
86
  to -= 1
87
87
  else
88
88
  # Calculate by elasped time
@@ -108,7 +108,7 @@ module SAAL
108
108
  newm = 12 - (-newm)
109
109
  newy -= 1
110
110
  end
111
- from = Time.utc(newy, newm, time.day, time.hour, time.min, time.sec)
111
+ Time.utc(newy, newm, time.day, time.hour, time.min, time.sec)
112
112
  end
113
113
  end
114
114
  end
data/lib/dbstore.rb CHANGED
@@ -1,5 +1,8 @@
1
1
  module SAAL
2
2
  class DBStore
3
+ # Only give out last_value if it's less than 5 min old
4
+ MAX_LAST_VAL_AGE = 5*60
5
+
3
6
  include Enumerable
4
7
  def initialize(conffile=SAAL::DBCONF)
5
8
  @dbopts = YAML::load(File.new(conffile))
@@ -11,9 +14,9 @@ module SAAL
11
14
  db_query "CREATE TABLE IF NOT EXISTS sensor_reads
12
15
  (sensor VARCHAR(100),
13
16
  date INT,
14
- value FLOAT,
15
- INDEX USING HASH (sensor),
16
- INDEX USING BTREE (date))"
17
+ value FLOAT) ENGINE=InnoDB"
18
+ db_query "ALTER TABLE sensor_reads ADD INDEX sensor_date_value (sensor,date,value) USING BTREE",
19
+ :ignoreerr => 1061
17
20
  end
18
21
 
19
22
  def db_wipe
@@ -38,6 +41,20 @@ module SAAL
38
41
  def maximum(sensor, from, to)
39
42
  db_range("MAX", sensor, from, to)
40
43
  end
44
+ def last_value(sensor)
45
+ db_query "SELECT date,value FROM sensor_reads
46
+ WHERE sensor = '#{db_quote(sensor.to_s)}'
47
+ AND date > '#{Time.now.utc.to_i - MAX_LAST_VAL_AGE}'
48
+ ORDER BY date DESC LIMIT 1" do |r|
49
+ if r.num_rows == 0
50
+ return nil
51
+ else
52
+ row = r.fetch_row
53
+ date, value = [row[0].to_i, row[1].to_f]
54
+ return value
55
+ end
56
+ end
57
+ end
41
58
 
42
59
  def each
43
60
  db_query "SELECT sensor,date,value FROM sensor_reads" do |r|
data/lib/dinrelay.rb CHANGED
@@ -55,6 +55,9 @@ module SAAL
55
55
  def set_state(num, state)
56
56
  @cachetime = nil
57
57
  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
58
61
  response != nil
59
62
  end
60
63
 
data/lib/saal.rb CHANGED
@@ -10,7 +10,7 @@ module SAAL
10
10
  DBCONF = CONFDIR+"database.yml"
11
11
  CHARTSCONF = CONFDIR+"charts.yml"
12
12
 
13
- VERSION = '0.2.14'
13
+ VERSION = '0.2.21'
14
14
  end
15
15
 
16
16
  require File.dirname(__FILE__)+'/dbstore.rb'
data/lib/sensor.rb CHANGED
@@ -11,30 +11,34 @@ module SAAL
11
11
  end
12
12
 
13
13
  class Sensor
14
- MAX_READ_TRIES = 5
15
-
16
- attr_reader :name, :description
14
+ attr_reader :name, :description, :numreads
15
+ attr_accessor :underlying
17
16
  def initialize(dbstore, name, underlying, defs, opts={})
18
17
  @dbstore = dbstore
19
18
  @name = name
20
19
  @underlying = underlying
21
20
  @description = defs['name']
22
21
  @mock_opts = {}
23
-
24
- # Reading correction settings
25
- @max_value = defs['max_value']
26
- @max_correctable = defs['max_correctable']
27
- @min_value = defs['min_value']
28
- @min_correctable = defs['min_correctable']
29
-
30
- @read_offset = if defs['altitude'] && @underlying.sensor_type == :pressure
31
- defs['altitude'].to_f/9.2
22
+
23
+ if defs['altitude'] && @underlying.sensor_type == :pressure
24
+ @read_offset = defs['altitude'].to_f/9.2
25
+ elsif defs['linear_offset']
26
+ @read_offset = defs['linear_offset'].to_f
27
+ else
28
+ @read_offset = 0.0
29
+ end
30
+
31
+ if defs['linear_multiplier']
32
+ @read_multiplier = defs['linear_multiplier'].to_f
32
33
  else
33
- 0.0
34
+ @read_multiplier = 1.0
34
35
  end
35
36
 
36
- # Outliercache
37
- @outliercache = opts[:no_outliercache] ? nil : OutlierCache.new
37
+ @set_type = defs['type'] ? defs['type'].to_sym : nil
38
+
39
+ @numreads = (defs['numreads']||1).to_i
40
+ @numreads = 1 if @numreads == 0
41
+ @numreads += 1 if @numreads.even?
38
42
  end
39
43
 
40
44
  def writeable?
@@ -42,22 +46,24 @@ module SAAL
42
46
  end
43
47
 
44
48
  def sensor_type
45
- @underlying.sensor_type
49
+ @set_type || @underlying.sensor_type
46
50
  end
47
51
 
48
52
  def read
49
- outlier_proof_read(false)
53
+ real_read(false)
50
54
  end
51
55
 
52
56
  def read_uncached
53
- outlier_proof_read(true)
57
+ real_read(true)
54
58
  end
55
59
 
56
60
  def write(value)
57
61
  if @mock_opts[:value]
58
62
  @mock_opts[:value] = value
59
63
  else
60
- @underlying.write(value)
64
+ ret = @underlying.write(value)
65
+ store_value
66
+ ret
61
67
  end
62
68
  end
63
69
 
@@ -76,9 +82,14 @@ module SAAL
76
82
  apply_offset @dbstore.maximum(@name, from, to)
77
83
  end
78
84
 
85
+ def last_value
86
+ return @mock_opts[:last_value] if @mock_opts[:last_value]
87
+ apply_offset @dbstore.last_value(@name)
88
+ end
89
+
79
90
  def store_value
80
- value = read_uncached
81
- @dbstore.write(@name, Time.now.utc.to_i, value-@read_offset) if value
91
+ value = real_read(true,false)
92
+ @dbstore.write(@name, Time.now.utc.to_i, value) if value
82
93
  end
83
94
 
84
95
  def mock_set(opts)
@@ -86,30 +97,20 @@ module SAAL
86
97
  end
87
98
 
88
99
  private
89
- def outlier_proof_read(uncached)
100
+ def real_read(uncached,offset=true)
90
101
  return @mock_opts[:value] if @mock_opts[:value]
91
- tries = 0
92
- value = nil
93
- begin
94
- tries += 1
95
- value = @underlying.read(uncached)
96
- break if value && @outliercache && @outliercache.validate(value)
97
- end while tries < MAX_READ_TRIES
98
- normalize(value)
102
+ values = (0..@numreads-1).map{@underlying.read(uncached)}
103
+ #FIXME: If we don't get all values give up and return the first value
104
+ if not values.all? {|v| v.instance_of?(Float) || v.instance_of?(Integer)}
105
+ value = values[0]
106
+ else
107
+ value = values.sort[@numreads/2]
108
+ end
109
+ offset ? apply_offset(value) : value
99
110
  end
100
-
111
+
101
112
  def apply_offset(v)
102
- v ? v+@read_offset : v
103
- end
104
-
105
- def normalize(value)
106
- apply_offset(if @max_value and value > @max_value
107
- (@max_correctable and value <= @max_correctable) ? @max_value : nil
108
- elsif @min_value and value < @min_value
109
- (@min_correctable and value >= @min_correctable) ? @min_value : nil
110
- else
111
- value
112
- end)
113
+ v ? v*@read_multiplier+@read_offset : v
113
114
  end
114
115
  end
115
116
  end
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.14'
10
- s.date = '2011-06-21'
9
+ s.version = '0.2.21'
10
+ s.date = '2013-04-14'
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
@@ -28,7 +28,7 @@ EOF
28
28
 
29
29
  s.executables = Dir.glob("bin/*").map{|f| f.gsub('bin/','')}
30
30
 
31
- s.add_dependency('ownet', [">= 0.1.0"])
31
+ s.add_dependency('ownet', [">= 0.2.0"])
32
32
  s.add_dependency('nokogiri')
33
33
  s.add_dependency('mysql')
34
34
 
@@ -50,16 +50,36 @@ class TestChartData < Test::Unit::TestCase
50
50
 
51
51
  # Test all the alignment functions underlying :last, :periods
52
52
  def self.assert_alignment_interval(num,periods,from,to, periodnames=nil,
53
- now = nil, extra=nil)
53
+ now = nil, extra=nil,timezone=nil)
54
54
  define_method("test_alignment_#{num}#{periods}#{extra.to_s}") do
55
+ ENV['TZ'] = timezone || "UTC"
55
56
  now = now || Time.utc(2010, 12, 30, 15, 38, 19)
56
57
  o = SAAL::ChartDataRange.new(:last => num, :periods => periods, :now => now)
57
58
  assert_equal [from.to_i, to.to_i], [o.from, o.to],
58
59
  "Expecting #{from.utc} - #{to.utc}\n"+
59
60
  "Got #{Time.at(o.from).utc} - #{Time.at(o.to).utc}"
60
61
  assert_equal periodnames, o.periodnames if periodnames
62
+ ENV['TZ'] = "UTC"
61
63
  end
62
64
  end
65
+ # Check for correct timezone handling
66
+ assert_alignment_interval(24, :hours, Time.utc(2010, 12, 29, 16, 0, 0),
67
+ Time.utc(2010, 12, 30, 15, 59, 59),
68
+ (17..23).map{|s| s.to_s}+(0..16).map{|s| s.to_s},
69
+ nil,"_timezone_test","UTC-1")
70
+ # Check for the timezone changing times (added hour)
71
+ assert_alignment_interval(24, :hours, Time.utc(2012, 10, 28, 0, 0, 0),
72
+ Time.utc(2012, 10, 28, 23, 59, 59),
73
+ ["1"]+(1..23).map{|s| s.to_s},
74
+ Time.utc(2012, 10, 28, 23, 59, 59),
75
+ "_changing_timezone_test","Europe/Lisbon")
76
+ # Check for the timezone changing times (removed hour)
77
+ assert_alignment_interval(24, :hours, Time.utc(2012, 3, 25, 0, 0, 0),
78
+ Time.utc(2012, 3, 25, 23, 59, 59),
79
+ ["0"]+(2..23).map{|s| s.to_s}+["0"],
80
+ Time.utc(2012, 3, 25, 23, 59, 59),
81
+ "_changing_timezone_test","Europe/Lisbon")
82
+
63
83
  assert_alignment_interval(24, :hours, Time.utc(2010, 12, 29, 16, 0, 0),
64
84
  Time.utc(2010, 12, 30, 15, 59, 59),
65
85
  (16..23).map{|s| s.to_s}+(0..15).map{|s| s.to_s})
data/test/dbstore_test.rb CHANGED
@@ -67,4 +67,24 @@ class TestFileStore < Test::Unit::TestCase
67
67
  assert_equal [["test_sensor", test_time, test_value]]*n,
68
68
  @dbstore.map{|sensor,time,value| [sensor,time,value]}
69
69
  end
70
+
71
+ def test_last_value
72
+ db_setup
73
+ now = Time.now.utc.to_i
74
+ test_values = [[now-10, 105.0],[now-5, 95.0],[now-2, 100.0],[now, 100.5]]
75
+ test_values.each do |values|
76
+ @dbstore.write(:test_sensor, *values)
77
+ end
78
+ assert_equal 100.5, @dbstore.last_value(:test_sensor)
79
+ end
80
+
81
+ def test_last_value_stale
82
+ db_setup
83
+ now = Time.now.utc.to_i - SAAL::DBStore::MAX_LAST_VAL_AGE - 100
84
+ test_values = [[now-10, 105.0],[now-5, 95.0],[now-2, 100.0],[now, 100.5]]
85
+ test_values.each do |values|
86
+ @dbstore.write(:test_sensor, *values)
87
+ end
88
+ assert_equal nil, @dbstore.last_value(:test_sensor)
89
+ end
70
90
  end
@@ -3,8 +3,25 @@ require 'webrick'
3
3
  require 'benchmark'
4
4
 
5
5
  class TestDINRelay < Test::Unit::TestCase
6
- SERVICE_OPTS = {:host => 'localhost', :port => 33333,
7
- :user => "someuser", :pass =>"somepass"}
6
+ def setup
7
+ @@serv_num ||= 0
8
+ @@serv_num += 1
9
+
10
+ @og=SAAL::DINRelay::OutletGroup.new(service_opts)
11
+ @vals={1=>"OFF",2=>"OFF",3=>"ON",4=>"OFF",5=>"ON",6=>"ON",7=>"ON",8=>"OFF"}
12
+ @rvals={1=>"ON",2=>"ON",3=>"OFF",4=>"ON",5=>"OFF",6=>"OFF",7=>"OFF",8=>"ON"}
13
+
14
+ defs = YAML::load(File.new(TEST_SENSORS_DINRELAY_FILE))
15
+ defs['group1']['dinrelay']['port'] = service_opts[:port]
16
+ tempfile = Tempfile.new('dinrelay_test_yml')
17
+ File.open(tempfile.path,'w') {|f| f.write(YAML::dump defs)}
18
+ @test_sensors_dinrelay_file = tempfile.path
19
+ end
20
+
21
+ def service_opts
22
+ base_opts = {:host => 'localhost', :user => "someuser", :pass =>"somepass"}
23
+ base_opts.merge(:port => 3333+@@serv_num)
24
+ end
8
25
 
9
26
  class BasicServing < WEBrick::HTTPServlet::AbstractServlet
10
27
  def self.get_instance(config, opts)
@@ -20,6 +37,8 @@ class TestDINRelay < Test::Unit::TestCase
20
37
  end
21
38
  def do_GET(req, res)
22
39
  sleep @sleep
40
+ @feedback[:uris] ||= []
41
+ @feedback[:uris] << req.request_uri.to_s
23
42
  @feedback[:uri] = req.request_uri.to_s
24
43
  @feedback[:nrequests] = (@feedback[:nrequests]||0)+1
25
44
  WEBrick::HTTPAuth.basic_auth(req, res, "My Realm") {|user, pass|
@@ -32,7 +51,7 @@ class TestDINRelay < Test::Unit::TestCase
32
51
  end
33
52
 
34
53
  def with_webrick(opts)
35
- opts = SERVICE_OPTS.merge(opts)
54
+ opts = service_opts.merge(opts)
36
55
 
37
56
  Socket.do_not_reverse_lookup = true # Speed up startup
38
57
  log = WEBrick::Log.new($stderr, WEBrick::Log::ERROR)
@@ -57,14 +76,8 @@ class TestDINRelay < Test::Unit::TestCase
57
76
  erb.result(binding)
58
77
  end
59
78
 
60
- def setup
61
- @og=SAAL::DINRelay::OutletGroup.new(SERVICE_OPTS)
62
- @vals={1=>"OFF",2=>"OFF",3=>"ON",4=>"OFF",5=>"ON",6=>"ON",7=>"ON",8=>"OFF"}
63
- @rvals={1=>"ON",2=>"ON",3=>"OFF",4=>"ON",5=>"OFF",6=>"OFF",7=>"OFF",8=>"ON"}
64
- end
65
-
66
79
  def assert_path(path, uri)
67
- assert_equal "http://localhost:#{SERVICE_OPTS[:port]}"+path, uri
80
+ assert_equal "http://localhost:#{service_opts[:port]}"+path, uri
68
81
  end
69
82
 
70
83
  def test_read_state
@@ -87,7 +100,7 @@ class TestDINRelay < Test::Unit::TestCase
87
100
  end
88
101
 
89
102
  def test_enumerate_sensors
90
- sensors = SAAL::Sensors.new(TEST_SENSORS_DINRELAY_FILE, TEST_DBCONF)
103
+ sensors = SAAL::Sensors.new(@test_sensors_dinrelay_file, TEST_DBCONF)
91
104
  assert_equal((1..8).map{|i| "name#{i}"}, sensors.map{|s| s.name}.sort)
92
105
  assert_equal((1..8).map{|i| "description#{i}"}, sensors.map{|s| s.description}.sort)
93
106
  end
@@ -99,7 +112,7 @@ class TestDINRelay < Test::Unit::TestCase
99
112
  end
100
113
 
101
114
  def test_read_sensors
102
- sensors = SAAL::Sensors.new(TEST_SENSORS_DINRELAY_FILE, TEST_DBCONF)
115
+ sensors = SAAL::Sensors.new(@test_sensors_dinrelay_file, TEST_DBCONF)
103
116
  with_webrick(:html=>create_index_html(@vals)) do |feedback|
104
117
  @vals.each do |num, state|
105
118
  value = state == "ON" ? 1.0 : 0.0
@@ -111,21 +124,22 @@ class TestDINRelay < Test::Unit::TestCase
111
124
  end
112
125
 
113
126
  def test_set_sensors
114
- sensors = SAAL::Sensors.new(TEST_SENSORS_DINRELAY_FILE, TEST_DBCONF)
127
+ sensors = SAAL::Sensors.new(@test_sensors_dinrelay_file, TEST_DBCONF)
115
128
  with_webrick(:html=>create_index_html(@rvals)) do |feedback|
116
129
  @vals.each do |num, state|
117
130
  newval = state == "ON" ? 0.0 : 1.0
118
131
  newstate = state == "ON" ? "OFF" : "ON"
119
132
  assert_equal newval, sensors.send('name'+num.to_s).write(newval),
120
133
  "State change not working"
121
- assert_path "/outlet?#{num}=#{newstate}", feedback[:uri]
134
+ assert_path "/outlet?#{num}=#{newstate}", feedback[:uris][-2]
135
+ assert_path "/index.htm", feedback[:uris][-1]
122
136
  end
123
137
  end
124
138
  end
125
139
 
126
140
  # Test that write invalidates any caching
127
141
  def test_write_read_sensors
128
- sensors = SAAL::Sensors.new(TEST_SENSORS_DINRELAY_FILE, TEST_DBCONF)
142
+ sensors = SAAL::Sensors.new(@test_sensors_dinrelay_file, TEST_DBCONF)
129
143
  with_webrick(:html=>create_index_html(@vals)) do |feedback|
130
144
  @vals.each do |num, state|
131
145
  sensors.send('name'+num.to_s).write(0.0)
@@ -137,7 +151,7 @@ class TestDINRelay < Test::Unit::TestCase
137
151
 
138
152
  # Test that the cache times out
139
153
  def test_cache_invalidation
140
- sensors = SAAL::Sensors.new(TEST_SENSORS_DINRELAY_FILE, TEST_DBCONF)
154
+ sensors = SAAL::Sensors.new(@test_sensors_dinrelay_file, TEST_DBCONF)
141
155
  @og.cache_timeout = 0.1
142
156
  with_webrick(:html=>create_index_html(@vals)) do |feedback|
143
157
  @og.state(1)
@@ -165,7 +179,7 @@ class TestDINRelay < Test::Unit::TestCase
165
179
 
166
180
  def test_fast_open_timeout
167
181
  #FIXME: Find a way to make this test address more generic
168
- @og=SAAL::DINRelay::OutletGroup.new(SERVICE_OPTS.merge(:host => "10.254.254.254",
182
+ @og=SAAL::DINRelay::OutletGroup.new(service_opts.merge(:host => "10.254.254.254",
169
183
  :timeout=>0.1))
170
184
  with_webrick(:html=>create_index_html(@vals)) do |feedback|
171
185
  time = Benchmark.measure do
@@ -181,7 +195,7 @@ class TestDINRelay < Test::Unit::TestCase
181
195
  end
182
196
 
183
197
  def test_fast_read_timeout
184
- @og=SAAL::DINRelay::OutletGroup.new(SERVICE_OPTS.merge(:timeout=>0.1))
198
+ @og=SAAL::DINRelay::OutletGroup.new(service_opts.merge(:timeout=>0.1))
185
199
  with_webrick(:html=>create_index_html(@vals),:sleep=>10) do |feedback|
186
200
  time = Benchmark.measure do
187
201
  @vals.each do |num, state|
data/test/sensor_test.rb CHANGED
@@ -1,13 +1,29 @@
1
1
  require File.dirname(__FILE__)+'/test_helper.rb'
2
2
 
3
3
  class MockConnection
4
- attr_accessor :value, :values
4
+ attr_accessor :value, :values, :stored_value
5
5
  def initialize
6
6
  @value = @values = nil
7
7
  end
8
8
  def read(serial)
9
9
  @value ? @value : @values.shift
10
10
  end
11
+ def write(serial, value)
12
+ @stored_value = value
13
+ end
14
+ end
15
+
16
+ class MockOWUnderlying
17
+ attr_accessor :value
18
+ def initialize(opts={})
19
+ @value = opts[:value]
20
+ end
21
+ def read(cached)
22
+ @value
23
+ end
24
+ def write(value)
25
+ @value = value
26
+ end
11
27
  end
12
28
 
13
29
  class MockDBStore
@@ -21,6 +37,9 @@ class MockDBStore
21
37
  def maximum(sensor, from, to)
22
38
  @value
23
39
  end
40
+ def last_value(sensor)
41
+ @value
42
+ end
24
43
  def write(sensor,date,value)
25
44
  @stored_value = value
26
45
  end
@@ -36,78 +55,51 @@ class TestSensor < Test::Unit::TestCase
36
55
  @defs = YAML::load File.new(TEST_SENSOR_CLEANUPS_FILE)
37
56
  @conn = MockConnection.new
38
57
  @dbstore = MockDBStore.new
39
- @fake = fake_sensor('fake', :no_outliercache => true)
40
- @fake2 = fake_sensor('fake2', :no_outliercache => true)
41
- @fake3 = fake_sensor('fake3')
42
- @max_value = @defs['fake2']['max_value']
43
- @max_correctable = @defs['fake2']['max_correctable']
44
- @min_value = @defs['fake2']['min_value']
45
- @min_correctable = @defs['fake2']['min_correctable']
46
- end
47
-
48
- def test_read_too_high_values
49
- @conn.value = @max_value+1
50
- assert_nil @fake.read
51
- assert_nil @fake.read_uncached
52
- @conn.value = @max_value
53
- assert_equal @max_value, @fake.read
54
- assert_equal @max_value, @fake.read_uncached
58
+ @fake = fake_sensor('fake')
55
59
  end
56
60
 
57
- def test_read_too_high_but_correctable_values
58
- @conn.value = @max_correctable
59
- assert_equal @max_value, @fake2.read
60
- assert_equal @max_value, @fake2.read_uncached
61
- @conn.value = @max_correctable+1
62
- assert_nil @fake2.read
63
- assert_nil @fake2.read_uncached
61
+ def test_last_value
62
+ @dbstore.value = 55.3
63
+ assert_equal 55.3, @fake.last_value
64
64
  end
65
65
 
66
- def test_read_too_low_values
67
- @conn.value = @min_value-1
68
- assert_nil @fake.read
69
- assert_nil @fake.read_uncached
70
- @conn.value = @min_value
71
- assert_equal @min_value, @fake.read
72
- assert_equal @min_value, @fake.read_uncached
66
+ def test_write_causes_store
67
+ @fake.underlying = MockOWUnderlying.new(:value => 5)
68
+ assert_equal 5, @fake.read
69
+ @fake.write(10)
70
+ assert_equal 10, @dbstore.stored_value
71
+ assert_equal 10, @fake.underlying.value
73
72
  end
74
73
 
75
- def test_read_too_low_but_correctable_values
76
- @conn.value = @min_correctable
77
- assert_equal @min_value, @fake2.read
78
- assert_equal @min_value, @fake2.read_uncached
79
- @conn.value = @min_correctable-1
80
- assert_nil @fake2.read
81
- assert_nil @fake2.read_uncached
74
+ def test_numreads
75
+ sensor = fake_sensor('fake5')
76
+ assert_equal 1, sensor.numreads
77
+ sensor = fake_sensor('fake4')
78
+ assert_equal 5, sensor.numreads
82
79
  end
83
80
 
84
- def test_read_without_limits
85
- @conn.value = 200
86
- assert_equal 200, @fake3.read
87
- assert_equal 200, @fake3.read_uncached
81
+ def test_no_outlier_removal
82
+ sensor = fake_sensor('fake')
83
+ @conn.values = [1000.0,1.0,1.0,1.0,1.0]
84
+ assert_equal 1000.0, sensor.read
88
85
  end
89
86
 
90
- def test_eliminate_outliers
91
- @conn.values = [200]*20 + [1000,200]
92
- assert_equal [200]*21, (1..21).map{@fake3.read}
93
- @conn.values = [200]*20 + [1000,200,1000,200]
94
- assert_equal [200]*21, (1..21).map{@fake3.read_uncached}
87
+ def test_single_outlier_removal
88
+ sensor = fake_sensor('fake2')
89
+ @conn.values = [1000.0,1.0,1.0,1.0,1.0]
90
+ assert_equal 1.0, sensor.read
95
91
  end
96
92
 
97
- def test_eliminate_outliers_zeroes
98
- @conn.values = [0]*20 + [1000,0]
99
- assert_equal [0]*20+[1000], (1..21).map{@fake3.read}
100
- @conn.values = [0]*20 + [1000,0]
101
- assert_equal [0]*20+[1000], (1..21).map{@fake3.read_uncached}
93
+ def test_double_outlier_removal
94
+ sensor = fake_sensor('fake3')
95
+ @conn.values = [1000.0,1000.0,1.0,1.0,1.0]
96
+ assert_equal 1.0, sensor.read
102
97
  end
103
98
 
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}
99
+ def test_outlier_removal_with_nils
100
+ sensor = fake_sensor('fake')
101
+ @conn.values = [1000.0,nil,1.0]
102
+ assert_equal 1000.0, sensor.read
111
103
  end
112
104
 
113
105
  def test_sealevel_correction
@@ -122,24 +114,42 @@ class TestSensor < Test::Unit::TestCase
122
114
  assert_equal corrected, sensor.average(0,100)
123
115
  end
124
116
 
117
+ def test_linear_correction
118
+ sensor = fake_sensor('offset')
119
+ @conn.value = @dbstore.value = 1000
120
+ corrected = @defs['offset']['linear_multiplier'].to_f*1000+
121
+ @defs['offset']['linear_offset'].to_f
122
+ assert_equal corrected, sensor.read
123
+ sensor.store_value
124
+ assert_equal 1000, @dbstore.stored_value
125
+ assert_equal corrected, sensor.minimum(0,100)
126
+ assert_equal corrected, sensor.maximum(0,100)
127
+ assert_equal corrected, sensor.average(0,100)
128
+ end
129
+
125
130
  def test_sensor_type
126
131
  [:pressure, :humidity, :temperature].each do |type|
127
132
  assert_equal type, fake_sensor(type.to_s).sensor_type
128
133
  end
129
134
  end
130
135
 
136
+ def test_set_sensor_type
137
+ assert_equal :temperature, fake_sensor("temperature_forced").sensor_type
138
+ end
139
+
131
140
  def test_mocked
132
- @mockable = fake_sensor('fake3')
141
+ @mockable = fake_sensor('fake')
133
142
  @conn.value = 1.0
134
143
  assert_equal 1.0, @mockable.read
135
144
  @mockable.mock_set(:value => 2.0)
136
145
  assert_equal 2.0, @mockable.read
137
146
  @mockable.write(3.0)
138
147
  assert_equal 3.0, @mockable.read
139
- @mockable.mock_set(:minimum => 1.0, :average => 2.0, :maximum => 3.0)
148
+ @mockable.mock_set(:minimum => 1.0, :average => 2.0, :maximum => 3.0, :last_value => 5.0)
140
149
  assert_equal 1.0, @mockable.minimum(0,100)
141
150
  assert_equal 2.0, @mockable.average(0,100)
142
151
  assert_equal 3.0, @mockable.maximum(0,100)
152
+ assert_equal 5.0, @mockable.last_value
143
153
  assert_equal 3.0, @mockable.read
144
154
  end
145
155
  end
data/test/sensors_test.rb CHANGED
@@ -31,7 +31,7 @@ class TestSensors < Test::Unit::TestCase
31
31
 
32
32
  def test_each
33
33
  expected = @defs.map{ |name, value| value['name']}
34
- assert_equal expected, @sensors.map {|sensor| sensor.description}
34
+ assert_equal expected.sort, @sensors.map {|sensor| sensor.description}.sort
35
35
  end
36
36
 
37
37
  def test_writeable
data/test/test_db.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  host: localhost
2
2
  user: sensor_reads
3
- pass: abcd
3
+ pass: password
4
4
  db: sensor_reads_test
@@ -1,24 +1,39 @@
1
1
  fake:
2
2
  name: "A fake temperature sensor"
3
- max_value: 500
4
- min_value: 100
5
3
  onewire:
6
4
  serial: /10.4AEC29CDBAAB/temperature
7
5
 
8
6
  fake2:
9
7
  name: "A fake temperature sensor"
10
- max_value: 500
11
- max_correctable: 550
12
- min_value: 100
13
- min_correctable: 50
8
+ numreads: 3
14
9
  onewire:
15
10
  serial: /10.4AEC29CDBAAB/temperature
16
11
 
17
12
  fake3:
18
13
  name: "A fake temperature sensor"
14
+ numreads: 5
19
15
  onewire:
20
16
  serial: /10.4AEC29CDBAAB/temperature
21
17
 
18
+ fake4:
19
+ name: "A fake temperature sensor"
20
+ numreads: 4
21
+ onewire:
22
+ serial: /10.4AEC29CDBAAB/temperature
23
+
24
+ fake5:
25
+ name: "A fake temperature sensor"
26
+ numreads: 0
27
+ onewire:
28
+ serial: /10.4AEC29CDBAAB/temperature
29
+
30
+ offset:
31
+ name: "A fake linear offset sensor"
32
+ linear_multiplier: 2
33
+ linear_offset: 100
34
+ onewire:
35
+ serial: /10.4AEC29CDBAAB/pressure
36
+
22
37
  pressure:
23
38
  name: "A fake pressure sensor"
24
39
  altitude: 200
@@ -33,5 +48,11 @@ humidity:
33
48
  onewire:
34
49
  serial: /10.4AEC29CDBAAB/humidity
35
50
 
51
+ temperature_forced:
52
+ name: "A fake temperature sensor"
53
+ type: temperature
54
+ onewire:
55
+ serial: /10.4AEC29CDBAAB/nondescript
56
+
36
57
 
37
58
 
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: 11
5
- prerelease: false
4
+ hash: 61
5
+ prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 2
9
- - 14
10
- version: 0.2.14
9
+ - 21
10
+ version: 0.2.21
11
11
  platform: ruby
12
12
  authors:
13
13
  - "Pedro C\xC3\xB4rte-Real"
@@ -15,8 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-06-21 00:00:00 +01:00
19
- default_executable:
18
+ date: 2013-04-14 00:00:00 Z
20
19
  dependencies:
21
20
  - !ruby/object:Gem::Dependency
22
21
  name: ownet
@@ -26,12 +25,12 @@ dependencies:
26
25
  requirements:
27
26
  - - ">="
28
27
  - !ruby/object:Gem::Version
29
- hash: 27
28
+ hash: 23
30
29
  segments:
31
30
  - 0
32
- - 1
31
+ - 2
33
32
  - 0
34
- version: 0.1.0
33
+ version: 0.2.0
35
34
  type: :runtime
36
35
  version_requirements: *id001
37
36
  - !ruby/object:Gem::Dependency
@@ -69,13 +68,14 @@ description: |
69
68
 
70
69
  email: pedro@pedrocr.net
71
70
  executables:
71
+ - saal_chart~
72
+ - dinrelaystatus
72
73
  - saal_readall
73
74
  - saal_dump_database
74
- - dinrelayset
75
- - dinrelaystatus
76
- - saal_import_mysql
77
75
  - saal_daemon
78
76
  - saal_chart
77
+ - dinrelayset
78
+ - saal_import_mysql
79
79
  extensions: []
80
80
 
81
81
  extra_rdoc_files:
@@ -123,7 +123,7 @@ files:
123
123
  - test/test_helper.rb
124
124
  - test/test_sensor_cleanups.yml
125
125
  - test/test_sensors.yml
126
- has_rdoc: true
126
+ - bin/saal_chart~
127
127
  homepage: https://github.com/pedrocr/saal
128
128
  licenses: []
129
129
 
@@ -156,7 +156,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
156
156
  requirements: []
157
157
 
158
158
  rubyforge_project:
159
- rubygems_version: 1.3.7
159
+ rubygems_version: 1.8.15
160
160
  signing_key:
161
161
  specification_version: 2
162
162
  summary: Thin abstraction layer for interfacing and recording sensors (currently onewire) and actuators (currently dinrelay)