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 +2 -1
- data/TODO +1 -1
- data/bin/saal_chart +10 -3
- data/bin/saal_chart~ +90 -0
- data/lib/chart_data.rb +3 -3
- data/lib/dbstore.rb +20 -3
- data/lib/dinrelay.rb +3 -0
- data/lib/saal.rb +1 -1
- data/lib/sensor.rb +43 -42
- data/saal.gemspec +3 -3
- data/test/chart_data_test.rb +21 -1
- data/test/dbstore_test.rb +20 -0
- data/test/dinrelay_test.rb +32 -18
- data/test/sensor_test.rb +71 -61
- data/test/sensors_test.rb +1 -1
- data/test/test_db.yml +1 -1
- data/test/test_sensor_cleanups.yml +27 -6
- metadata +14 -14
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 '
|
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
|
-
|
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
|
-
|
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,
|
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.
|
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
|
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
|
-
|
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
|
-
|
16
|
-
|
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
data/lib/saal.rb
CHANGED
data/lib/sensor.rb
CHANGED
@@ -11,30 +11,34 @@ module SAAL
|
|
11
11
|
end
|
12
12
|
|
13
13
|
class Sensor
|
14
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
34
|
+
@read_multiplier = 1.0
|
34
35
|
end
|
35
36
|
|
36
|
-
|
37
|
-
|
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
|
-
|
53
|
+
real_read(false)
|
50
54
|
end
|
51
55
|
|
52
56
|
def read_uncached
|
53
|
-
|
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 =
|
81
|
-
@dbstore.write(@name, Time.now.utc.to_i, 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
|
100
|
+
def real_read(uncached,offset=true)
|
90
101
|
return @mock_opts[:value] if @mock_opts[:value]
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
end
|
98
|
-
|
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.
|
10
|
-
s.date = '
|
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.
|
31
|
+
s.add_dependency('ownet', [">= 0.2.0"])
|
32
32
|
s.add_dependency('nokogiri')
|
33
33
|
s.add_dependency('mysql')
|
34
34
|
|
data/test/chart_data_test.rb
CHANGED
@@ -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
|
data/test/dinrelay_test.rb
CHANGED
@@ -3,8 +3,25 @@ require 'webrick'
|
|
3
3
|
require 'benchmark'
|
4
4
|
|
5
5
|
class TestDINRelay < Test::Unit::TestCase
|
6
|
-
|
7
|
-
|
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 =
|
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:#{
|
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(
|
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(
|
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(
|
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[:
|
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(
|
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(
|
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(
|
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(
|
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'
|
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
|
58
|
-
@
|
59
|
-
assert_equal
|
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
|
67
|
-
@
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
assert_equal
|
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
|
76
|
-
|
77
|
-
assert_equal
|
78
|
-
|
79
|
-
|
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
|
85
|
-
|
86
|
-
|
87
|
-
assert_equal
|
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
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
105
|
-
|
106
|
-
|
107
|
-
|
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('
|
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,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
|
-
|
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:
|
5
|
-
prerelease:
|
4
|
+
hash: 61
|
5
|
+
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 2
|
9
|
-
-
|
10
|
-
version: 0.2.
|
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:
|
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:
|
28
|
+
hash: 23
|
30
29
|
segments:
|
31
30
|
- 0
|
32
|
-
-
|
31
|
+
- 2
|
33
32
|
- 0
|
34
|
-
version: 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
|
-
|
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.
|
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)
|