saal 0.2.14 → 0.2.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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)