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 +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)
|