plantwatchdog 0.0.1

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.
Files changed (50) hide show
  1. data/History.txt +4 -0
  2. data/License.txt +674 -0
  3. data/Manifest.txt +42 -0
  4. data/README.txt +91 -0
  5. data/Rakefile +22 -0
  6. data/bin/plantwatchdog +8 -0
  7. data/bin/upload_measurements +2 -0
  8. data/config.ru +35 -0
  9. data/config/app_config.yaml +10 -0
  10. data/lib/plantwatchdog/aggregation.rb +220 -0
  11. data/lib/plantwatchdog/aggregation_methods.rb +90 -0
  12. data/lib/plantwatchdog/data.rb +126 -0
  13. data/lib/plantwatchdog/db.rb +37 -0
  14. data/lib/plantwatchdog/gems.rb +5 -0
  15. data/lib/plantwatchdog/main.rb +76 -0
  16. data/lib/plantwatchdog/model.rb +442 -0
  17. data/lib/plantwatchdog/sinatra.rb +206 -0
  18. data/public/images/arrow-down.gif +0 -0
  19. data/public/images/arrow-left.gif +0 -0
  20. data/public/images/arrow-right.gif +0 -0
  21. data/public/images/arrow-up.gif +0 -0
  22. data/public/images/spinner.gif +0 -0
  23. data/public/images/tabs.png +0 -0
  24. data/public/js/customflot.js +120 -0
  25. data/public/js/jquery-1.3.2.min.js +19 -0
  26. data/public/js/jquery.flot.crosshair.js +157 -0
  27. data/public/js/jquery.flot.js +2119 -0
  28. data/public/js/jquery.flot.navigate.js +272 -0
  29. data/public/js/jquery.flot.selection.js +299 -0
  30. data/public/js/select-chain.js +71 -0
  31. data/public/js/tools.tabs-1.0.4.js +285 -0
  32. data/public/tabs.css +87 -0
  33. data/sample/solar/create_solar.rb +31 -0
  34. data/sample/solar/measurements/client.sqlite3 +0 -0
  35. data/sample/solar/static/devices.yml +17 -0
  36. data/sample/solar/static/metadata.yml +30 -0
  37. data/sample/solar/static/plants.yml +3 -0
  38. data/sample/solar/static/users.yml +4 -0
  39. data/sample/solar/upload_measurements +26 -0
  40. data/templates/graph.erb +134 -0
  41. data/templates/index.erb +24 -0
  42. data/templates/monthly_graph.erb +41 -0
  43. data/test/test_aggregation.rb +161 -0
  44. data/test/test_aggregation_methods.rb +50 -0
  45. data/test/test_base.rb +83 -0
  46. data/test/test_data.rb +118 -0
  47. data/test/test_model.rb +142 -0
  48. data/test/test_sync.rb +71 -0
  49. data/test/test_web.rb +87 -0
  50. metadata +167 -0
@@ -0,0 +1,42 @@
1
+ config.ru
2
+ History.txt
3
+ License.txt
4
+ Manifest.txt
5
+ README.txt
6
+ Rakefile
7
+ bin/plantwatchdog
8
+ bin/upload_measurements
9
+ config/app_config.yaml
10
+ lib/plantwatchdog/aggregation.rb
11
+ lib/plantwatchdog/aggregation_methods.rb
12
+ lib/plantwatchdog/data.rb
13
+ lib/plantwatchdog/db.rb
14
+ lib/plantwatchdog/gems.rb
15
+ lib/plantwatchdog/main.rb
16
+ lib/plantwatchdog/model.rb
17
+ lib/plantwatchdog/sinatra.rb
18
+ public/images/arrow-down.gif
19
+ public/images/arrow-left.gif
20
+ public/images/arrow-right.gif
21
+ public/images/arrow-up.gif
22
+ public/images/spinner.gif
23
+ public/images/tabs.png
24
+ public/js/customflot.js
25
+ public/js/jquery-1.3.2.min.js
26
+ public/js/jquery.flot.crosshair.js
27
+ public/js/jquery.flot.js
28
+ public/js/jquery.flot.navigate.js
29
+ public/js/jquery.flot.selection.js
30
+ public/js/select-chain.js
31
+ public/js/tools.tabs-1.0.4.js
32
+ public/tabs.css
33
+ sample/solar/create_solar.rb
34
+ sample/solar/measurements/client.sqlite3
35
+ sample/solar/static/devices.yml
36
+ sample/solar/static/metadata.yml
37
+ sample/solar/static/plants.yml
38
+ sample/solar/static/users.yml
39
+ sample/solar/upload_measurements
40
+ templates/graph.erb
41
+ templates/index.erb
42
+ templates/monthly_graph.erb
@@ -0,0 +1,91 @@
1
+ = Plant Watchdog
2
+
3
+ A watchdog for your technical plant, e.g. a photovoltaic generator. The plant
4
+ watchdog listens to data loggers, which continuously upload measurements of
5
+ plant parameters. Based on that data the watchdog creates reports about the
6
+ operational status.
7
+
8
+
9
+ == DESCRIPTION:
10
+
11
+ This software is being developed to monitor a photovoltaic generator. We found
12
+ that it would make sense to keep the domain specific knowledge out of the code
13
+ and instead provide a DSL to allow users to define their rules. Therefore this
14
+ software should be useful to monitor any kind of technical plant.
15
+
16
+ At the time being the focus of development is to
17
+ - make it reasonably stable
18
+ - run per-day aggregations
19
+ - show intraday and monthly diagrams
20
+
21
+ With more data being available the task of finding deviations from regular
22
+ operation is becoming feasible. Therefore expected values of parameters must
23
+ be calculated and compared with actual values: the users must be allowed to
24
+ define a model of the plant.
25
+
26
+ == FEATURES/PROBLEMS:
27
+
28
+ The vision of this software is to
29
+ - allow data logger devices to continuously upload time series measurements
30
+ - backup and distribute measurement data
31
+ - run user defined aggregations and analyses on the data
32
+ - provide customizable HTML reports including visual diagrams
33
+ - find deviations from regular operation and send alarms if necessary
34
+
35
+ == REQUIREMENTS:
36
+
37
+ * Ruby 1.8.7 and a database supported by active record, e.g. MySQL or SQLite
38
+
39
+ == INSTALL:
40
+
41
+ 1. Install Ruby 1.8
42
+ For ubuntu
43
+ $ apt-get install ruby1.8-dev libopenssl-ruby1.8
44
+
45
+ 2. Install a database
46
+ Any database supported from active record should do, so far the server has
47
+ been tested with MySql 5.1.14 and SQLite 3.6.21.
48
+
49
+ In order to install sqlite3 and ruby bindings on ubuntu run
50
+ $ apt-get install sqlite3-dev
51
+ $ gem install sqlite3-ruby
52
+
53
+ 3. Install Plant Watchdog and required gems
54
+ There are two options, either install the gem or download the sources from
55
+ github.
56
+ A) gem
57
+ $ gem install plantwatchdog
58
+
59
+ B) github
60
+ $ mkdir plant
61
+ $ cd plant
62
+ $ git clone git@github.com:mbarchfe/plantwatchdog.git
63
+ $ rake check_extra_deps // Get the required ruby gems
64
+
65
+ 4. Install sample database and run
66
+ If your gem's bin directory is not on the PATH, add it
67
+ $ export PATH=$PATH:/var/lib/gems/1.8/bin
68
+
69
+ The default database connection uses sqlite3 and a database at
70
+ /tmp/solarsample.sqlite3. You can change the default by editing
71
+ the config file
72
+ $ vi config/app-config.yaml
73
+
74
+ Install the sample
75
+ $ plantwatch --create_sample
76
+ Upload data (needs curl and sqlite3 command line)
77
+
78
+ Start the plantwatchdog web server (an alternative way is to start via rackup)
79
+ $ plantwatchdog
80
+
81
+ On another shell upload sample measurements. The script needs curl and the
82
+ sqlite3 command line
83
+ $ apt-get install curl
84
+ $ upload_measurements
85
+
86
+ Run the daily aggregation
87
+ $ plantwatchdog -a
88
+
89
+ == LICENSE:
90
+
91
+ GPL v3
@@ -0,0 +1,22 @@
1
+ # -*- ruby -*-
2
+ $:.unshift File.join(File.dirname(__FILE__),"lib")
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require 'plantwatchdog/main'
6
+
7
+ Hoe.spec 'plantwatchdog' do |p|
8
+ developer('plantwatchdogteam', 'mbarchfe@rubyforge.org')
9
+ p.version=PlantWatchdog::Version::STRING
10
+ p.rubyforge_name = 'pwd'
11
+ p.author = "Plant Watchdog Team"
12
+ p.email = "mbarchfe@rubyforge.org"
13
+ p.summary = 'Plant Watchdog'
14
+ p.description = p.paragraphs_of('README.txt', 1..5).join("\n\n")
15
+ p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
16
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
17
+ p.extra_deps<<['sinatra','0.9.4']
18
+ p.extra_deps<<['activerecord', '2.3.5']
19
+ p.extra_deps<<['patir', '0.6.4']
20
+ p.spec_extras={:executables=>["plantwatchdog","upload_measurements"],
21
+ :default_executable=>"plantwatchdog"}
22
+ end
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # an alternative way of starting the server is via rackup:
3
+ # 1. bin$ gem install rack
4
+ # 2. bin$ cd ..
5
+ # 3. $ rackup
6
+ #d=`dirname $0`
7
+ #ruby -I${d:-.}/../lib -rubygems ${d:-.}/../lib/plantwatchdog/main.rb $*
8
+ require 'plantwatchdog/main'
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env ruby
2
+ exec File.join(File.dirname(__FILE__),"..", "sample", "solar", "upload_measurements")
@@ -0,0 +1,35 @@
1
+ # this is Rackup file. It is necessary for applications started from a container
2
+ # e.g. passenger, or via the rackup command.
3
+ require 'rubygems'
4
+ #this locks on the versions we need
5
+ require 'lib/plantwatchdog/gems'
6
+ require 'lib/plantwatchdog/sinatra.rb'
7
+ require 'lib/plantwatchdog/db.rb'
8
+ require 'yaml'
9
+ path = ''
10
+
11
+ set :root, path
12
+ set :views, path + '/views'
13
+ set :public, path + '/public'
14
+ set :run, false
15
+ set :raise_errors, true
16
+
17
+ log = File.new("sinatra.log", "a")
18
+ STDOUT.reopen(log)
19
+ STDERR.reopen(log)
20
+
21
+ logger=Logger.new(STDOUT)
22
+ logger.level = Logger::INFO
23
+
24
+ config_file="config/app_config.yaml"
25
+ if File.exists?(config_file)
26
+ config=YAML.load(File.read(config_file))
27
+ config[:logger]=logger
28
+ else
29
+ logger.fatal("Cannot find #{config_file}")
30
+ exit 1
31
+ end
32
+
33
+ extend PlantWatchdog::ActiveRecordConnections
34
+ self.connect_to_active_record(config[:database_configuration],logger)
35
+ run PlantWatchdog::UI::SinatraApp
@@ -0,0 +1,10 @@
1
+ #---
2
+ #:database_configuration:
3
+ # :adapter: mysql
4
+ # :database: solar
5
+ # :username: root
6
+ # :password: root
7
+
8
+ :database_configuration:
9
+ :adapter: sqlite3
10
+ :database: /tmp/solarsample.sqlite3
@@ -0,0 +1,220 @@
1
+ # Copyright (C) 2010 Markus Barchfeld, Vassilis Rizopoulos
2
+ #
3
+ # This program is free software; you can redistribute it and/or modify it under
4
+ # the terms of the GNU General Public License as published by the Free Software
5
+ # Foundation; either version 3 of the License, or (at your option) any later
6
+ # version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but WITHOUT
9
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
10
+ # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
11
+ #
12
+ # You should have received a copy of the GNU General Public License along with
13
+ # this program; if not, see <http://www.gnu.org/licenses/>.
14
+
15
+ $:.unshift File.join(File.dirname(__FILE__),"..")
16
+ require 'plantwatchdog/model'
17
+ require 'plantwatchdog/aggregation_methods'
18
+
19
+ module PlantWatchdog
20
+ # define the Aggregation Model,
21
+ module Aggregation
22
+ # the global environment to which the aggregation blocks have access
23
+ class AggregationEnv
24
+ logger = ActiveRecord::Base.logger
25
+ attr_accessor :year, :day_of_year, :plant, :devices
26
+ def initialize(year, day_of_year)
27
+ @year = year
28
+ @day_of_year = day_of_year
29
+ @devices = []
30
+ end
31
+
32
+ end
33
+
34
+ class Data
35
+ # return the time series data for symbol
36
+ def [] symbol
37
+
38
+ end
39
+
40
+ # return the list of seconds of the day for which there is data available
41
+ def times
42
+ end
43
+ end
44
+
45
+ module RuleEvaluation
46
+ # data is an array of dictionaries, e.g. retrieved via Measurements.from_csv
47
+ def eval_rule rule_array, data
48
+ return 0 if rule_array.empty?
49
+ method_name = rule_array.first
50
+ arg_desc = rule_array[1, rule_array.size] # the column names for which we need to create time series
51
+ args = arg_desc.collect { |arg_desc|
52
+ if arg_desc.is_a? Numeric then
53
+ arg_desc
54
+ elsif arg_desc.is_a? Array
55
+ eval_rule(arg_desc, data)
56
+ else
57
+ data.collect { |d| d[arg_desc] }
58
+ end
59
+ }
60
+ return Methods.call(method_name, *args)
61
+ end
62
+ end
63
+
64
+ class Device
65
+ include RuleEvaluation
66
+ def Device.create(model_device, year, day)
67
+ # select data
68
+ data = Model::MeasurementChunk.find(:first, :conditions => ["device_id=? and time_year=? and time_day_of_year=?", model_device.id, year, day])
69
+ Device.new(model_device, data)
70
+ end
71
+ attr_accessor :model_device
72
+
73
+ def initialize(model_device, data)
74
+ @model_device = model_device
75
+ @aggregates = {}
76
+ @data = data
77
+ end
78
+
79
+ # generic access to the fields of the underlying model_device
80
+ def not_understand
81
+ # model_device
82
+ end
83
+
84
+ def measurements
85
+ @data ? @data.measurements : []
86
+ end
87
+
88
+ def meta
89
+ model_device.meta
90
+ end
91
+
92
+ # return the dict with aggregated values
93
+ def aggregates
94
+
95
+ end
96
+
97
+ # execute the aggregation rules of the device
98
+ def aggregate
99
+ result = {}
100
+ logger.debug "Aggregating device #{model_device.id}, aggrules: #{model_device.aggrules}"
101
+ model_device.aggrules.each_pair do
102
+ |agg_key, rule_array|
103
+ result[agg_key] = eval_rule(rule_array, measurements)
104
+ end
105
+ logger.debug "Aggregation results: " + result.to_s
106
+ result
107
+ end
108
+
109
+ def persist
110
+ dm = Model::DailyMeasurement.new()
111
+ dm.description = JSON(aggregates)
112
+ return dm
113
+ end
114
+
115
+ # TODO: better way to access logger
116
+ def logger
117
+ return ActiveRecord::Base.logger
118
+ end
119
+ end
120
+
121
+ class Plant
122
+ include RuleEvaluation
123
+ def initialize model_plant, device_aggregates
124
+ @model_plant = model_plant
125
+ @device_aggregates = device_aggregates
126
+ end
127
+
128
+ def aggregate
129
+ result = {}
130
+ logger.debug "Aggregating plant, aggrules: #{@model_plant.aggrules}"
131
+ @model_plant.aggrules.each_pair do
132
+ |agg_key, rule_array|
133
+ result[agg_key] = eval_rule(rule_array, @device_aggregates)
134
+ end
135
+ logger.debug "Aggregation results: " + result.to_s
136
+ result
137
+ end
138
+
139
+ # TODO: better way to access logger
140
+ def logger
141
+ return ActiveRecord::Base.logger
142
+ end
143
+
144
+ end
145
+
146
+ class Runner
147
+ def find_missing_aggregates
148
+ sql = <<EOF
149
+ select time_year, time_day_of_year, plant_id from
150
+ (select CHUNK.time_year, CHUNK.time_day_of_year, CHUNK.device_id AS device_id, AGG.device_id AS agg_device_id from
151
+ ((select time_year, time_day_of_year, device_id from measurement_chunks where type="MeasurementChunk" order by time_year, time_day_of_year, device_id) AS CHUNK
152
+ LEFT OUTER JOIN
153
+ (select time_year, time_day_of_year, device_id from measurement_chunks where type="MeasurementAggregate") AS AGG
154
+ ON CHUNK.time_year = AGG.time_year AND CHUNK.time_day_of_year=AGG.time_day_of_year AND CHUNK.device_id = AGG.device_id)
155
+ where AGG.device_id IS NULL) AS MISSING
156
+ INNER JOIN
157
+ devices
158
+ ON MISSING.device_id = devices.id
159
+ GROUP BY MISSING.time_year, MISSING.time_day_of_year;
160
+ EOF
161
+ rows = ActiveRecord::Base.connection.select_all(sql)
162
+ result = []
163
+ rows.collect {
164
+ |r|
165
+ time_year = r["time_year"].to_i
166
+ time_day_of_year = r["time_day_of_year"].to_i
167
+ plant_id = r["plant_id"].to_i
168
+ [time_year, time_day_of_year, plant_id]
169
+ }
170
+ end
171
+
172
+ def run
173
+ find_missing_aggregates.each {
174
+ |m|
175
+ time_year, time_day_of_year, plant_id = m
176
+ aggregate(Model::Plant.find_by_id(plant_id), time_year, time_day_of_year)
177
+ }
178
+ end
179
+
180
+ def aggregate(model_plant, year, day_of_year)
181
+ env = AggregationEnv.new(year, day_of_year)
182
+ model_plant.devices.each {
183
+ |model_device|
184
+ logger.debug("Adding device " + model_device.to_s)
185
+ env.devices << Device.create(model_device, year, day_of_year)
186
+ }
187
+ # build the aggregates for the devices first ...
188
+ aggregates = env.devices.collect do
189
+ |device|
190
+ daily = Model::MeasurementAggregate.new
191
+ daily.device = device.model_device
192
+ daily.time_year = year
193
+ daily.time_day_of_year = day_of_year
194
+ daily.data = device.aggregate
195
+ daily
196
+ end
197
+
198
+ # ... and then aggregate the plant
199
+ gen_aggregates = Plant.new(model_plant, aggregates.collect{|ma| ma.data}).aggregate
200
+
201
+ gen_aggregate = Model::MeasurementAggregate.new
202
+ gen_aggregate.data = gen_aggregates
203
+ gen_aggregate.time_year = year
204
+ gen_aggregate.time_day_of_year = day_of_year
205
+ gen_aggregate.plant = model_plant
206
+
207
+ # save when everything has been calculated
208
+ aggregates << gen_aggregate
209
+ aggregates.each {|a| a.save}
210
+ aggregates
211
+ end
212
+
213
+ # TODO: better way to access logger
214
+ def logger
215
+ return ActiveRecord::Base.logger
216
+ end
217
+
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,90 @@
1
+ # Copyright (C) 2010 Markus Barchfeld, Vassilis Rizopoulos
2
+ #
3
+ # This program is free software; you can redistribute it and/or modify it under
4
+ # the terms of the GNU General Public License as published by the Free Software
5
+ # Foundation; either version 3 of the License, or (at your option) any later
6
+ # version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but WITHOUT
9
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
10
+ # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
11
+ #
12
+ # You should have received a copy of the GNU General Public License along with
13
+ # this program; if not, see <http://www.gnu.org/licenses/>.
14
+
15
+ $:.unshift File.join(File.dirname(__FILE__),"..")
16
+ require 'plantwatchdog/model'
17
+
18
+ class Array
19
+ def each_prior()
20
+ i=0
21
+ result=[]
22
+ while (i < self.size-1) do
23
+ result << yield(self[i],self[i+1])
24
+ i+=1
25
+ end
26
+ return result
27
+ end
28
+ end
29
+
30
+ module PlantWatchdog
31
+ module Aggregation
32
+ module Methods
33
+ class << self
34
+ def growth timeseries
35
+ timeseries.last - timeseries.first
36
+ end
37
+
38
+ def avg timeseries
39
+ result = sum(timeseries)
40
+ result.to_f / timeseries.size
41
+ end
42
+
43
+ def integrate times, values
44
+ return [times.each_prior {|x,y| y-x}, values.each_prior {|x,y| (y+x)/2.0}].transpose.inject(0) {|i,a| i + a.first * a.last }
45
+ end
46
+
47
+ def sum timeseries
48
+ result = 0
49
+ timeseries.each { |v| result += v if v}
50
+ return result
51
+ end
52
+
53
+ def mult a,b
54
+ a*b
55
+ end
56
+
57
+ def div a,b
58
+ a/b
59
+ end
60
+
61
+ def subtract a,b
62
+ a - b
63
+ end
64
+
65
+ def add
66
+ a + b
67
+ end
68
+
69
+ def pick n,a
70
+ a[n]
71
+ end
72
+
73
+ def call(method, *args)
74
+ begin
75
+ m = Methods.method method
76
+ m.call *args
77
+ rescue
78
+ logger.debug("Error calling method '#{method}': " + $!.to_s)
79
+ nil
80
+ end
81
+ end
82
+
83
+ def logger
84
+ return ActiveRecord::Base.logger
85
+ end
86
+ end
87
+
88
+ end
89
+ end
90
+ end