saal 0.2.2
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/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
|