saal 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +460 -0
- data/README.rdoc +76 -0
- data/Rakefile +160 -0
- data/TODO +24 -0
- data/bin/.gitignore +2 -0
- data/bin/dinrelayset +35 -0
- data/bin/dinrelaystatus +32 -0
- data/bin/saal_chart +110 -0
- data/bin/saal_daemon +22 -0
- data/bin/saal_dump_database +11 -0
- data/bin/saal_import_mysql +18 -0
- data/bin/saal_readall +10 -0
- data/lib/chart_data.rb +30 -0
- data/lib/daemon.rb +63 -0
- data/lib/dbstore.rb +86 -0
- data/lib/dinrelay.rb +67 -0
- data/lib/outliercache.rb +53 -0
- data/lib/owsensor.rb +21 -0
- data/lib/saal.rb +23 -0
- data/lib/sensor.rb +100 -0
- data/lib/sensors.rb +48 -0
- data/saal.gemspec +77 -0
- data/test/chart_data_test.rb +39 -0
- data/test/daemon_test.rb +44 -0
- data/test/dbstore_test.rb +70 -0
- data/test/dinrelay.html.erb +143 -0
- data/test/dinrelay_test.rb +112 -0
- data/test/nonexistant_sensor.yml +5 -0
- data/test/outliercache_test.rb +42 -0
- data/test/sensor_test.rb +109 -0
- data/test/sensors_test.rb +77 -0
- data/test/test_db.yml +4 -0
- data/test/test_dinrelay_sensors.yml +16 -0
- data/test/test_helper.rb +37 -0
- data/test/test_sensor_cleanups.yml +21 -0
- data/test/test_sensors.yml +10 -0
- metadata +166 -0
data/Rakefile
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
# Based on the jekyll Rakefile (http://github.com/mojombo/jekyll)
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'rake'
|
5
|
+
require 'date'
|
6
|
+
require 'rcov/rcovtask'
|
7
|
+
require 'rake/testtask'
|
8
|
+
require 'rake/rdoctask'
|
9
|
+
|
10
|
+
#############################################################################
|
11
|
+
#
|
12
|
+
# Helper functions
|
13
|
+
#
|
14
|
+
#############################################################################
|
15
|
+
|
16
|
+
def name
|
17
|
+
@name ||= Dir['*.gemspec'].first.split('.').first
|
18
|
+
end
|
19
|
+
|
20
|
+
def version
|
21
|
+
line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/]
|
22
|
+
line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
|
23
|
+
end
|
24
|
+
|
25
|
+
def date
|
26
|
+
Date.today.to_s
|
27
|
+
end
|
28
|
+
|
29
|
+
def rubyforge_project
|
30
|
+
name
|
31
|
+
end
|
32
|
+
|
33
|
+
def gemspec_file
|
34
|
+
"#{name}.gemspec"
|
35
|
+
end
|
36
|
+
|
37
|
+
def gem_file
|
38
|
+
"#{name}-#{version}.gem"
|
39
|
+
end
|
40
|
+
|
41
|
+
def replace_header(head, header_name)
|
42
|
+
head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"}
|
43
|
+
end
|
44
|
+
|
45
|
+
#############################################################################
|
46
|
+
#
|
47
|
+
# Standard tasks
|
48
|
+
#
|
49
|
+
#############################################################################
|
50
|
+
|
51
|
+
task :default => [:test]
|
52
|
+
|
53
|
+
Rake::TestTask.new(:test) do |test|
|
54
|
+
test.libs << 'lib' << 'test'
|
55
|
+
test.pattern = 'test/*_test.rb'
|
56
|
+
test.verbose = true
|
57
|
+
end
|
58
|
+
|
59
|
+
Rcov::RcovTask.new(:coverage) do |t|
|
60
|
+
t.libs << "test"
|
61
|
+
t.test_files = FileList['test/*_test.rb']
|
62
|
+
t.rcov_opts << ['--exclude "^/"', '--include "lib/.*\.rb"']
|
63
|
+
t.output_dir = 'coverage'
|
64
|
+
t.verbose = true
|
65
|
+
end
|
66
|
+
|
67
|
+
Rake::RDocTask.new do |rdoc|
|
68
|
+
rdoc.rdoc_dir = 'rdoc'
|
69
|
+
rdoc.title = "#{name} #{version}"
|
70
|
+
rdoc.rdoc_files.include('README*')
|
71
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
72
|
+
end
|
73
|
+
|
74
|
+
desc "Open an irb session preloaded with this library"
|
75
|
+
task :console do
|
76
|
+
sh "irb -rubygems -r ./lib/#{name}.rb"
|
77
|
+
end
|
78
|
+
|
79
|
+
desc "Prints code to test ratio stats"
|
80
|
+
task :stats do
|
81
|
+
CODE_FILES = "lib/**/*.rb"
|
82
|
+
TEST_FILES = "test/*_test.rb"
|
83
|
+
|
84
|
+
code_code, code_comments = count_lines(FileList[CODE_FILES])
|
85
|
+
test_code, test_comments = count_lines(FileList[TEST_FILES])
|
86
|
+
|
87
|
+
puts "Code lines: #{code_code} code, #{code_comments} comments"
|
88
|
+
puts "Test lines: #{test_code} code, #{test_comments} comments"
|
89
|
+
|
90
|
+
ratio = test_code.to_f/code_code.to_f
|
91
|
+
|
92
|
+
puts "Code to test ratio: 1:%.2f" % ratio
|
93
|
+
end
|
94
|
+
|
95
|
+
def count_lines(files)
|
96
|
+
code = 0
|
97
|
+
comments = 0
|
98
|
+
files.each do |f|
|
99
|
+
File.open(f).each do |line|
|
100
|
+
if line.strip[0] == '#'[0]
|
101
|
+
comments += 1
|
102
|
+
else
|
103
|
+
code += 1
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
[code, comments]
|
108
|
+
end
|
109
|
+
|
110
|
+
#############################################################################
|
111
|
+
#
|
112
|
+
# Packaging tasks
|
113
|
+
#
|
114
|
+
#############################################################################
|
115
|
+
|
116
|
+
desc "git tag, build and release gem"
|
117
|
+
task :release => :build do
|
118
|
+
unless `git branch` =~ /^\* master$/
|
119
|
+
puts "You must be on the master branch to release!"
|
120
|
+
exit!
|
121
|
+
end
|
122
|
+
sh "git commit --allow-empty -a -m 'Release #{version}'"
|
123
|
+
sh "git tag v#{version}"
|
124
|
+
sh "git push origin master"
|
125
|
+
sh "git push origin v#{version}"
|
126
|
+
sh "gem push pkg/#{name}-#{version}.gem"
|
127
|
+
end
|
128
|
+
|
129
|
+
desc "Build gem"
|
130
|
+
task :build => :gemspec do
|
131
|
+
sh "mkdir -p pkg"
|
132
|
+
sh "gem build #{gemspec_file}"
|
133
|
+
sh "mv #{gem_file} pkg"
|
134
|
+
end
|
135
|
+
|
136
|
+
task :gemspec do
|
137
|
+
# read spec file and split out manifest section
|
138
|
+
spec = File.read(gemspec_file)
|
139
|
+
head, manifest, tail = spec.split(" # = MANIFEST =\n")
|
140
|
+
|
141
|
+
# replace name version and date
|
142
|
+
replace_header(head, :name)
|
143
|
+
replace_header(head, :version)
|
144
|
+
replace_header(head, :date)
|
145
|
+
|
146
|
+
# determine file list from git ls-files
|
147
|
+
files = `git ls-files`.
|
148
|
+
split("\n").
|
149
|
+
sort.
|
150
|
+
reject { |file| file =~ /^\./ }.
|
151
|
+
reject { |file| file =~ /^(rdoc|pkg|coverage)/ }.
|
152
|
+
map { |file| " #{file}" }.
|
153
|
+
join("\n")
|
154
|
+
|
155
|
+
# piece file back together and write
|
156
|
+
manifest = " s.files = %w[\n#{files}\n ]\n"
|
157
|
+
spec = [head, manifest, tail].join(" # = MANIFEST =\n")
|
158
|
+
File.open(gemspec_file, 'w') { |io| io.write(spec) }
|
159
|
+
puts "Updated #{gemspec_file}"
|
160
|
+
end
|
data/TODO
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
TODO
|
2
|
+
- Add logging to the daemon
|
3
|
+
?- Change the sensor configuration to be a ruby DSL and make it a daemon config
|
4
|
+
- Split classes into one per file with corresponding test (rails style)
|
5
|
+
- Verify inputs on the server to make sure it never crashes
|
6
|
+
- Remove the chart configuration options from the saal_chart script
|
7
|
+
- Add an init.d file to the package (and possibly an installer script for ubuntu/debian)
|
8
|
+
- Add outlier detection and removal
|
9
|
+
DONE
|
10
|
+
X- Make the server bind only to a certain interface (not applicable)
|
11
|
+
X- Override OWNet::Connection with a mock object so that owserver is not needed
|
12
|
+
X- Implement GET_ALL
|
13
|
+
X- Multithread the server
|
14
|
+
X- Make connections persistant
|
15
|
+
- Find a better way to handle stdin, stdout, stderr for tests (an option maybe)
|
16
|
+
- Implement the charts
|
17
|
+
- Write client library for server part of daemon
|
18
|
+
- Implement AVERAGE
|
19
|
+
- Make sensor reads uncached
|
20
|
+
- Refactor daemon.rb to pull out its daemon creating code from the process
|
21
|
+
- Create a client/server framework to ask the server for values
|
22
|
+
- Make the date returned by GET the date of the last read (GET doesn't return
|
23
|
+
a date now
|
24
|
+
- Implement monthly, yearly and 10-day average charts
|
data/bin/.gitignore
ADDED
data/bin/dinrelayset
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.dirname(__FILE__)+'/../lib/saal.rb'
|
4
|
+
|
5
|
+
def exit_usage
|
6
|
+
$stderr.puts "Usage: dinrelayset <host:[port]> <port 1-8> <ON|OFF> [user:password]"
|
7
|
+
exit 2
|
8
|
+
end
|
9
|
+
|
10
|
+
if ARGV.size > 4 || ARGV.size < 3
|
11
|
+
exit_usage
|
12
|
+
end
|
13
|
+
|
14
|
+
opts = {}
|
15
|
+
|
16
|
+
hp = ARGV[0].split(":")
|
17
|
+
opts[:host] = hp[0]
|
18
|
+
opts[:port] = (hp[1] || 80).to_i
|
19
|
+
|
20
|
+
num = ARGV[1].to_i
|
21
|
+
exit_usage if num < 1 || num > 8
|
22
|
+
state = ARGV[2]
|
23
|
+
exit_usage if state != 'ON' && state != 'OFF'
|
24
|
+
|
25
|
+
if ARGV[4]
|
26
|
+
up = ARGV[4].split(":")
|
27
|
+
exit_usage if up.size != 2
|
28
|
+
opts[:user] = up[0]
|
29
|
+
opts[:pass] = up[1]
|
30
|
+
end
|
31
|
+
|
32
|
+
puts "Setting outlet #{num} of #{opts[:host]}:#{opts[:port]} to #{state}"
|
33
|
+
og = SAAL::DINRelay::OutletGroup.new(opts)
|
34
|
+
og.set_state(num, state)
|
35
|
+
|
data/bin/dinrelaystatus
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.dirname(__FILE__)+'/../lib/saal.rb'
|
4
|
+
|
5
|
+
def exit_usage
|
6
|
+
$stderr.puts "Usage: dinrelaystatus <host:[port]> [user:password]"
|
7
|
+
exit 2
|
8
|
+
end
|
9
|
+
|
10
|
+
if ARGV.size > 2 || ARGV.size < 1
|
11
|
+
exit_usage
|
12
|
+
end
|
13
|
+
|
14
|
+
opts = {}
|
15
|
+
|
16
|
+
hp = ARGV[0].split(":")
|
17
|
+
opts[:host] = hp[0]
|
18
|
+
opts[:port] = (hp[1] || 80).to_i
|
19
|
+
|
20
|
+
if ARGV[1]
|
21
|
+
up = ARGV[1].split(":")
|
22
|
+
exit_usage if up.size != 2
|
23
|
+
opts[:user] = up[0]
|
24
|
+
opts[:pass] = up[1]
|
25
|
+
end
|
26
|
+
|
27
|
+
puts "Checking outlets for #{opts[:host]}:#{opts[:port]}"
|
28
|
+
og = SAAL::DINRelay::OutletGroup.new(opts)
|
29
|
+
(1..8).each do |num|
|
30
|
+
puts "Outlet \##{num}: #{og.state(num).to_s}"
|
31
|
+
end
|
32
|
+
|
data/bin/saal_chart
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.dirname(__FILE__)+'/../lib/saal.rb'
|
4
|
+
|
5
|
+
def usage
|
6
|
+
$stderr.puts("Usage: saal_chart <day|week|4week|year|4year> <chart_file.png>")
|
7
|
+
end
|
8
|
+
|
9
|
+
if ARGV.size != 2
|
10
|
+
usage
|
11
|
+
exit (2)
|
12
|
+
end
|
13
|
+
|
14
|
+
@now = Time.now.utc
|
15
|
+
|
16
|
+
NUM_VALUES = 500
|
17
|
+
case ARGV[0]
|
18
|
+
when 'day' then
|
19
|
+
PERIODNAMES = (0..23).map{|i| ((@now.hour - i)%24).to_s}.reverse
|
20
|
+
ALIGNMENT = :hour
|
21
|
+
NUMDAYS = 1
|
22
|
+
ALIGNNAMES = :center
|
23
|
+
when 'week' then
|
24
|
+
daynames = ["Seg","Ter","Qua","Qui","Sex","Sab","Dom"]
|
25
|
+
PERIODNAMES = (1..7).map{|i| (@now.wday - i)%7}.map{|w| daynames[w]}.reverse
|
26
|
+
ALIGNMENT = :day
|
27
|
+
NUMDAYS = 7
|
28
|
+
ALIGNNAMES = :center
|
29
|
+
when '4week' then
|
30
|
+
monthnames = ["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Sep","Out","Nov","Dez"]
|
31
|
+
initial = @now.to_i - (@now.wday-1)*24*60*60
|
32
|
+
PERIODNAMES = (0...4).map do |i|
|
33
|
+
time = Time.at(initial - i*24*60*60*7)
|
34
|
+
time.day.to_s+" "+ monthnames[time.month-1]
|
35
|
+
end.reverse
|
36
|
+
ALIGNMENT = :week
|
37
|
+
NUMDAYS = 28
|
38
|
+
ALIGNNAMES = :left
|
39
|
+
when 'year' then
|
40
|
+
monthnames = ["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Sep","Out","Nov","Dez"]
|
41
|
+
initial = @now.to_i - (@now.wday-1)*24*60*60
|
42
|
+
PERIODNAMES = (1..12).map{|i| (@now.month - i)%12}.map{|m| monthnames[m]}.reverse
|
43
|
+
ALIGNMENT = :month
|
44
|
+
NUMDAYS = 356
|
45
|
+
ALIGNNAMES = :center
|
46
|
+
when '4year' then
|
47
|
+
PERIODNAMES = (0..3).map{|i| (@now.year - i).to_s}.reverse
|
48
|
+
ALIGNMENT = :year
|
49
|
+
NUMDAYS = 356*3
|
50
|
+
ALIGNNAMES = :center
|
51
|
+
else
|
52
|
+
usage
|
53
|
+
exit(3)
|
54
|
+
end
|
55
|
+
NUMPERIODS = PERIODNAMES.size
|
56
|
+
|
57
|
+
align = {:year => [12,31,23,59,59],
|
58
|
+
:month => [31,23,59,59],
|
59
|
+
:day => [23,59,59],
|
60
|
+
:week => [23,59,59],
|
61
|
+
:hour => [59,59]}
|
62
|
+
|
63
|
+
args = [@now.year, @now.month, @now.day, @now.hour, @now.min, @now.sec]
|
64
|
+
args = args[0..-(align[ALIGNMENT].size+1)]
|
65
|
+
args += align[ALIGNMENT]
|
66
|
+
@to = Time.utc(*args).to_i
|
67
|
+
@to += (6-@now.wday)*60*60*24 if ALIGNMENT == :week
|
68
|
+
@from = @to - 60*60*24*NUMDAYS
|
69
|
+
|
70
|
+
@sensors = SAAL::Sensors.new
|
71
|
+
def create_data(sensor, min, max, constant=0)
|
72
|
+
@c = SAAL::ChartData.new(@sensors.send(sensor))
|
73
|
+
d = @c.get_data(@from, @to, NUM_VALUES)
|
74
|
+
d = d.map{|num| num ? num+constant : num}
|
75
|
+
@c.normalize_data(d,min,max)
|
76
|
+
end
|
77
|
+
|
78
|
+
@data = [create_data('temp_exterior', -15, 45),
|
79
|
+
create_data('temp_estufa', -15, 45),
|
80
|
+
create_data('hum_exterior', 0, 100),
|
81
|
+
create_data('pressao', 950, 1050, 0.54*33.86)] #Convert to pressure at sea level
|
82
|
+
|
83
|
+
|
84
|
+
@dataurl = @data.map {|values| values.join(",")}.join('|')
|
85
|
+
|
86
|
+
r = {}
|
87
|
+
case ALIGNNAMES
|
88
|
+
when :center
|
89
|
+
@periodnamesurl = "||"+PERIODNAMES.join('||')+"||"
|
90
|
+
when :left
|
91
|
+
@periodnamesurl = "|"+PERIODNAMES.join('|')+"||"
|
92
|
+
r[:chxs] = "0,555555,11,-1,lt"
|
93
|
+
end
|
94
|
+
@xincr = 100.0/NUMPERIODS.to_f*10000.truncate.to_f/10000
|
95
|
+
|
96
|
+
r[:chof] = "png"
|
97
|
+
r[:chs] = "700x300"
|
98
|
+
r[:cht] = "lc"
|
99
|
+
r[:chco] = "00ff00,ff0000,0000ff,ffff00"
|
100
|
+
r[:chxt] = "x,y,y,r"
|
101
|
+
r[:chxl] = "0:#{@periodnamesurl}1:|-15ºC||0||15||30||45ºC|2:|0%|25|50|75|100%|3:|950||975||1000||1025||1050 hPa"
|
102
|
+
r[:chg] = "#{@xincr},12.5,1,5"
|
103
|
+
r[:chd] = "t:#{@dataurl}"
|
104
|
+
|
105
|
+
@url = "http://chart.apis.google.com/chart?&"
|
106
|
+
@postdata = r.map{|k,v| k.to_s+"="+v}.join("&")
|
107
|
+
|
108
|
+
|
109
|
+
system "wget --quiet \"#{@url}\" --post-data=\"#{@postdata}\" -O #{ARGV[1]}"
|
110
|
+
|
data/bin/saal_daemon
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
SENSOR_INTERVAL = 60 # seconds between consecutive measurements of the sensors
|
4
|
+
DBCONF = "/etc/saal/database.yml"
|
5
|
+
SENSORSCONF = "/etc/saal/sensors.yml"
|
6
|
+
|
7
|
+
require File.dirname(__FILE__)+'/../lib/saal.rb'
|
8
|
+
|
9
|
+
if ARGV.size != 1
|
10
|
+
$stderr.puts "Usage: saal_daemon [pidfile]"
|
11
|
+
exit 2
|
12
|
+
else
|
13
|
+
pidfile = ARGV[0]
|
14
|
+
d = SAAL::Daemon.new(:interval => SENSOR_INTERVAL,
|
15
|
+
:sensorconf => SENSORSCONF,
|
16
|
+
:dbconf => DBCONF)
|
17
|
+
pid = d.run
|
18
|
+
File.open(pidfile, 'w') do |f|
|
19
|
+
f.write(pid)
|
20
|
+
f.close
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.dirname(__FILE__)+'/../lib/saal.rb'
|
4
|
+
|
5
|
+
begin
|
6
|
+
dbstore = SAAL::DBStore.new
|
7
|
+
|
8
|
+
nrows = 0
|
9
|
+
while not $stdin.eof?
|
10
|
+
values = $stdin.readline.split(" ").map{|s| s.strip}
|
11
|
+
if values[2] != ""
|
12
|
+
dbstore.write(values[0], values[1].to_i, values[2].to_f)
|
13
|
+
nrows += 1
|
14
|
+
end
|
15
|
+
end
|
16
|
+
puts "Number of rows inserted: #{nrows}"
|
17
|
+
end
|
18
|
+
|
data/bin/saal_readall
ADDED
data/lib/chart_data.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
module SAAL
|
2
|
+
class ChartData
|
3
|
+
def initialize(sensor)
|
4
|
+
@sensor = sensor
|
5
|
+
end
|
6
|
+
|
7
|
+
def get_data(from, to, num)
|
8
|
+
step = (to - from).to_f/num.to_f
|
9
|
+
(0..num-2).map do |i|
|
10
|
+
f = (from+i*step).to_i
|
11
|
+
t = (from+(i+1)*step-0.5).to_i
|
12
|
+
@sensor.average(f, t)
|
13
|
+
end << @sensor.average((from+(num-1)*step).to_i, to)
|
14
|
+
end
|
15
|
+
|
16
|
+
def normalize_data(data, min, max)
|
17
|
+
data.map do |i|
|
18
|
+
if i.nil?
|
19
|
+
-1.0
|
20
|
+
elsif i < min
|
21
|
+
0.0
|
22
|
+
elsif i > max
|
23
|
+
100.0
|
24
|
+
else
|
25
|
+
v = (((i-min)/(max-min).to_f)*1000).round/10.0
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/daemon.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
module SAAL
|
2
|
+
class ForkedRunner
|
3
|
+
def self.run_as_fork(opts={})
|
4
|
+
fork do
|
5
|
+
if not opts[:keep_stdin]
|
6
|
+
$stderr.reopen "/dev/null", "a"
|
7
|
+
$stdin.reopen "/dev/null", "a"
|
8
|
+
$stdout.reopen "/dev/null", "a"
|
9
|
+
end
|
10
|
+
yield ForkedRunner.new
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@rd, @wr = IO.pipe
|
16
|
+
@stop = false
|
17
|
+
trap_signals
|
18
|
+
end
|
19
|
+
|
20
|
+
def trap_signals
|
21
|
+
Signal.trap("TERM") {do_exit}
|
22
|
+
Signal.trap("INT") {do_exit}
|
23
|
+
end
|
24
|
+
|
25
|
+
def do_exit
|
26
|
+
@stop = true
|
27
|
+
@wr.write(1)
|
28
|
+
end
|
29
|
+
|
30
|
+
def stop?
|
31
|
+
@stop
|
32
|
+
end
|
33
|
+
|
34
|
+
def sleep(time)
|
35
|
+
select([],[],[],time)
|
36
|
+
end
|
37
|
+
|
38
|
+
def select(read, write=[], err=[], time=nil)
|
39
|
+
if time
|
40
|
+
Kernel.select(read+[@rd],write,err,time)
|
41
|
+
else
|
42
|
+
Kernel.select(read+[@rd],write,err)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class Daemon
|
48
|
+
def initialize(opts={})
|
49
|
+
@opts = opts
|
50
|
+
end
|
51
|
+
|
52
|
+
def run
|
53
|
+
ForkedRunner.run_as_fork(@opts) do |forked_runner|
|
54
|
+
@sensors = SAAL::Sensors.new(@opts[:sensorconf], @opts[:dbconf])
|
55
|
+
@interval = @opts[:interval] || 60
|
56
|
+
begin
|
57
|
+
@sensors.each {|sensor| sensor.store_value}
|
58
|
+
forked_runner.sleep @interval
|
59
|
+
end while !forked_runner.stop?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/dbstore.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
module SAAL
|
2
|
+
class DBStore
|
3
|
+
include Enumerable
|
4
|
+
def initialize(conffile=SAAL::DBCONF)
|
5
|
+
@dbopts = YAML::load(File.new(conffile))
|
6
|
+
@db = nil
|
7
|
+
db_initialize
|
8
|
+
end
|
9
|
+
|
10
|
+
def db_initialize
|
11
|
+
db_query "CREATE TABLE IF NOT EXISTS sensor_reads
|
12
|
+
(sensor VARCHAR(100),
|
13
|
+
date INT,
|
14
|
+
value FLOAT,
|
15
|
+
INDEX USING HASH (sensor),
|
16
|
+
INDEX USING BTREE (date))"
|
17
|
+
end
|
18
|
+
|
19
|
+
def db_wipe
|
20
|
+
db_query "DROP TABLE sensor_reads"
|
21
|
+
end
|
22
|
+
|
23
|
+
def write(sensor, date, value)
|
24
|
+
raise ArgumentError, "Trying to store an empty sensor read" if !value
|
25
|
+
raise ArgumentError, "Trying to store an empty timestamp" if !date
|
26
|
+
raise ArgumentError, "Trying to store a timestamp <= 0" if date <= 0
|
27
|
+
db_query "INSERT INTO sensor_reads VALUES
|
28
|
+
('"+db_quote(sensor.to_s)+"',"+date.to_s+","+value.to_s+")"
|
29
|
+
end
|
30
|
+
|
31
|
+
def average(sensor, from, to)
|
32
|
+
db_range("AVG", sensor, from, to)
|
33
|
+
end
|
34
|
+
|
35
|
+
def minimum(sensor, from, to)
|
36
|
+
db_range("MIN", sensor, from, to)
|
37
|
+
end
|
38
|
+
def maximum(sensor, from, to)
|
39
|
+
db_range("MAX", sensor, from, to)
|
40
|
+
end
|
41
|
+
|
42
|
+
def each
|
43
|
+
db_query "SELECT sensor,date,value FROM sensor_reads" do |r|
|
44
|
+
r.num_rows.times do
|
45
|
+
row = r.fetch_row
|
46
|
+
yield [row[0],row[1].to_i, row[2].to_f]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
def db_range(function, sensor, from, to)
|
53
|
+
db_query "SELECT #{function}(value) AS average FROM sensor_reads
|
54
|
+
WHERE sensor = '#{db_quote(sensor.to_s)}'
|
55
|
+
AND date >= #{from.to_s}
|
56
|
+
AND date <= #{to.to_s}" do |r|
|
57
|
+
if r.num_rows == 0
|
58
|
+
nil
|
59
|
+
else
|
60
|
+
row = r.fetch_row
|
61
|
+
row[0] ? row[0].to_f : nil
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def db_quote(text)
|
67
|
+
Mysql.quote(text)
|
68
|
+
end
|
69
|
+
|
70
|
+
def db_query(query)
|
71
|
+
db = nil
|
72
|
+
begin
|
73
|
+
# connect to the MySQL server
|
74
|
+
db = Mysql.new(@dbopts['host'],@dbopts['user'],@dbopts['pass'],
|
75
|
+
@dbopts['db'],@dbopts['port'],@dbopts['socket'],
|
76
|
+
@dbopts['flags'])
|
77
|
+
res = db.query(query)
|
78
|
+
yield res if block_given?
|
79
|
+
rescue Mysql::Error => e
|
80
|
+
$stderr.puts "MySQL Error \#{e.errno}: \#{e.error}"
|
81
|
+
ensure
|
82
|
+
db.close if db
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/dinrelay.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
module SAAL
|
4
|
+
module DINRelay
|
5
|
+
class Outlet < SensorUnderlying
|
6
|
+
writeable!
|
7
|
+
|
8
|
+
def initialize(num, outletgroup)
|
9
|
+
@num = num
|
10
|
+
@og = outletgroup
|
11
|
+
end
|
12
|
+
|
13
|
+
def read(uncached = false)
|
14
|
+
{'ON' => 1.0, 'OFF' => 0.0}[@og.state(@num)]
|
15
|
+
end
|
16
|
+
|
17
|
+
def write(value)
|
18
|
+
newstate = {1.0 => 'ON', 0.0 => 'OFF'}[value]
|
19
|
+
if newstate
|
20
|
+
@og.set_state(@num,newstate)
|
21
|
+
value
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class OutletGroup
|
27
|
+
def initialize(opts={})
|
28
|
+
@host = opts[:host] || opts['host'] || 'localhost'
|
29
|
+
@port = opts[:port] || opts['port'] || 80
|
30
|
+
@user = opts[:user] || opts['user'] || 'admin'
|
31
|
+
@pass = opts[:pass] || opts['pass'] || '1234'
|
32
|
+
end
|
33
|
+
|
34
|
+
def state(num)
|
35
|
+
response = do_get('/index.htm')
|
36
|
+
return parse_index_html(response.body)[num]
|
37
|
+
end
|
38
|
+
|
39
|
+
def set_state(num, state)
|
40
|
+
response = do_get("/outlet?#{num}=#{state}")
|
41
|
+
response.code == "200"
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
def do_get(path)
|
46
|
+
Net::HTTP.start(@host,@port) do |http|
|
47
|
+
req = Net::HTTP::Get.new(path)
|
48
|
+
req.basic_auth @user, @pass
|
49
|
+
response = http.request(req)
|
50
|
+
if response.code != "200"
|
51
|
+
$stderr.puts "ERROR: Code #{response.code}"
|
52
|
+
$stderr.puts response.body
|
53
|
+
end
|
54
|
+
return response
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def parse_index_html(str)
|
59
|
+
doc = Nokogiri::HTML(str)
|
60
|
+
outlets = doc.css('tr[bgcolor="#F4F4F4"]')
|
61
|
+
Hash[*((outlets.enum_for(:each_with_index).map do |el, index|
|
62
|
+
[index+1, el.css('font')[0].content]
|
63
|
+
end).flatten)]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|