plantwatchdog 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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,17 @@
1
+ inverter1:
2
+ plant: default_plant
3
+ unique_id: "2100147669"
4
+ metadata: inverter_meta
5
+ aggregationrule: inverter_aggrules
6
+
7
+ inverter2:
8
+ plant: default_plant
9
+ unique_id: "2100147715"
10
+ metadata: inverter_meta
11
+ aggregationrule: inverter_aggrules
12
+
13
+ sunmeter:
14
+ plant: default_plant
15
+ unique_id: "sunmeter"
16
+ metadata: sunmeter_meta
17
+ aggregationrule: sunmeter_aggrules
@@ -0,0 +1,30 @@
1
+ plant_aggrules:
2
+ description: '{
3
+ eday : [sum, eday],
4
+ pac : [sum, pac],
5
+ irradiance : [pick, 0, irradiance],
6
+ temperature : [pick, 0, temperature],
7
+ inv0_eday : [pick, 1, eday],
8
+ inv1_eday : [pick, 2, eday]
9
+ }'
10
+
11
+ inverter_meta:
12
+ description: '[[time, integer], [pac, float], [etotal, float]]'
13
+
14
+ # pac is W, integrated W*sec, divide by 3600 to get W*h, divide by 1000 to get kw*h
15
+ inverter_aggrules:
16
+ description: '{
17
+ eday : [growth, etotal],
18
+ pac : [div, [integrate, time, pac], 3600000]
19
+ }'
20
+
21
+ sunmeter_meta:
22
+ description: '[[time, integer], [irradiance, float], [temperature, integer]]'
23
+
24
+ # irradiance is W/qm, integrated W/qm*sec, divide by 3600 to get W/qm*h, multiply by 80qm to get W*h, divide by 1000 to get kw*h
25
+ # 1/10th => Umsetzung
26
+ sunmeter_aggrules:
27
+ description: '{
28
+ irradiance : [mult, 80, [div, [integrate, time, irradiance], 36000000]],
29
+ temperature : [avg, temperature]
30
+ }'
@@ -0,0 +1,3 @@
1
+ default_plant:
2
+ user: default_user
3
+ aggregationrule: plant_aggrules
@@ -0,0 +1,4 @@
1
+ default_user:
2
+ name: markus
3
+ password: markus
4
+ timezone: Europe/Berlin
@@ -0,0 +1,26 @@
1
+ #!/bin/sh
2
+ # this script uploads measurement data to a plant watchdog server
3
+ # To prepare the server you can run
4
+ # $ plantwatchdog --create_solar
5
+ # which creates an appropriate schema.
6
+ # Then start the server and run this script with the approriate host and port
7
+ host=${1:-127.0.0.1}
8
+ port=${2:-7000}
9
+ client_db=${3:-`dirname "$0"`/measurements/client.sqlite3}
10
+ csv_file=/tmp/measurements.csv
11
+ baseurl=http://$host:$port
12
+ user=markus
13
+ pw=markus
14
+ echo Upload measurements from $client_db to plant watchdog at $baseurl using $user:$pw
15
+
16
+ ids=`sqlite3 $client_db "select distinct inverterid from inverters"`
17
+ sqlite3 -csv $client_db 'select time, irradiance, temperature from environment' > $csv_file
18
+ count=`curl --user $user:$pw -T $csv_file $baseurl/upload/device/sunmeter`
19
+ echo Uploaded $count sunmeter measurements
20
+ for id in $ids
21
+ do
22
+ sqlite3 -csv $client_db "select time, pac, etotal from inverters where inverterid=$id" > $csv_file
23
+ count=`curl --user $user:$pw -T $csv_file $baseurl/upload/device/$id`
24
+ echo Uploaded $count measurements of inverter $id
25
+ done
26
+ rm $csv_file
@@ -0,0 +1,134 @@
1
+ <style>
2
+ #placeholder .button {
3
+ position: absolute;
4
+ cursor: pointer;
5
+ }
6
+ #placeholder div.button {
7
+ font-size: smaller;
8
+ color: #999;
9
+ background-color: #eee;
10
+ padding: 2px;
11
+ }
12
+ .loading {
13
+ background: url(/images/spinner.gif) no-repeat center center;
14
+ }
15
+ </style>
16
+ <link rel="stylesheet" href="tabs.css" type="text/css" />
17
+ <ul class="tabs">
18
+ <li><a href="#"><span>Intraday</span></a></li>
19
+ <li><a href="#"><span>Monthly report</span></a></li>
20
+ </ul>
21
+ <div class="panes">
22
+ <div id="intraday">
23
+ <form id="graph_form">
24
+ <label for="select_graph_year">Year:</label></td>
25
+ <select name="year" size="1" id="select_graph_year">
26
+ <% years = years_with_data(user);
27
+ years.each_index {
28
+ |i| %>
29
+ <option <%=i == years.size() -1 ? 'selected="selected"' : ""%>><%=years[i].to_s%></option>
30
+ <% } %>
31
+ </select>
32
+ <label for="select_graph_month">Month:</label></td>
33
+ <select name="month" size="1" id="select_graph_month">
34
+ </select>
35
+ <label for="select_graph_day">Day:</label></td>
36
+ <select name="day" size="1" id="select_graph_day">
37
+ </select>
38
+
39
+ <input class="fetchSeries" type="submit" value="Draw">
40
+ <br></br>
41
+ <!-- TODO: this is the view definition. It should not take place here but should be
42
+ generated from a view definition DSL from ruby -->
43
+ <input type="radio" name="radio_type" value="0" id="intraday_graph_type_inverter" checked="checked"/>
44
+ <label for="intraday_graph_type_inverter">Inverter</label>
45
+ <input type="radio" name="radio_type" value="1" id="intraday_graph_type_environment"/>
46
+ <label for="intraday_graph_type_environment">Environment</label>
47
+
48
+ </form>
49
+ <div id="placeholder" style="width:<%=graph_width%>;height:<%=graph_height%>;"></div>
50
+ </div>
51
+ <div id="graph_monthly">
52
+ </div>
53
+ </div>
54
+
55
+ <script src="/js/jquery-1.3.2.min.js" type="text/javascript"></script>
56
+ <script id="source" language="javascript" type="text/javascript">
57
+ prepare_graph = function () {
58
+ var year = $('#select_graph_year');
59
+ var month = $('#select_graph_month');
60
+ var day = $('#select_graph_day');
61
+ month.selectChain({
62
+ target: day,
63
+ type: 'get',
64
+ data: { }
65
+ },
66
+ function(settings) { settings.url = '/availabledata/'+$('#select_graph_year').selectedOption() + '/' + $('#select_graph_month').selectedOption() ;}
67
+ );
68
+
69
+ // note that we're assigning in reverse order
70
+ // to allow the chaining change trigger to work
71
+ year.selectChain({
72
+ target: month,
73
+ type: 'get',
74
+ data: {}
75
+ },
76
+ function(settings) { settings.url = '/availabledata/'+$('#select_graph_year').selectedOption();}
77
+ ).trigger('change');
78
+ day.change(function() {
79
+ if (month.get(0).options.selectedIndex >= 0) {
80
+ $('#graph_form:first').submit();
81
+ }
82
+ }) ;
83
+
84
+
85
+ var intraday = new Graph("#placeholder");
86
+ intraday.urlfunction = function() { return '/rawdata/' + $('#select_graph_year').selectedOption() + '/' + $('#select_graph_month').selectedOption() + '/' + $('#select_graph_day').selectedOption() };
87
+ $("form#graph_form").submit(function() { return intraday.submitForm() } );
88
+
89
+ // TODO: move view definition to ruby
90
+ // provide two views on the same data set
91
+ var set_intraday_filter=function()
92
+ {
93
+ if ($("input[@name='radio_type']:checked").val()==0)
94
+ intraday.seriesFilter=function(series) { return series["label"].indexOf("temperature") == -1; }
95
+ else
96
+ intraday.seriesFilter=function(series) { return series["label"].match("temperature|irradiance") != null; }
97
+ }
98
+ set_intraday_filter();
99
+ $("form#graph_form input:radio").click(function() { set_intraday_filter(); intraday.customPlot(); });
100
+
101
+ // Tabs
102
+ var api = $("ul.tabs").tabs("div.panes > div");
103
+ api = $("ul.tabs").tabs();
104
+ api.onClick(function(event, index) {
105
+ if (index==1) {
106
+ $.ajax( { url: "monthly_graph.html?width="+intraday.placeholder.css("width")+"&height=" + intraday.placeholder.css("height"),
107
+ context: document.body,
108
+ success: function(data, textStatus, XMLHttpRequest) {
109
+ $("#graph_monthly").html(data);
110
+ }
111
+ } );
112
+ } else {
113
+
114
+ }
115
+ });
116
+ };
117
+
118
+ var scripts = [ "js/select-chain.js", "js/jquery.flot.crosshair.js", "js/jquery.flot.selection.js",
119
+ "js/jquery.flot.navigate.js", "js/tools.tabs-1.0.4.js", "js/customflot.js" ]
120
+ var loaded = 0;
121
+ function load_other_scripts() {
122
+ $.each( scripts,
123
+ function(i,script) {
124
+ $.getScript(script, function() {
125
+ loaded += 1;
126
+ if (loaded == scripts.length) {
127
+ prepare_graph();
128
+ }
129
+ });
130
+ });
131
+ };
132
+ // make sure that flot is loaded before the other scripts, since some of them depend on it
133
+ $.getScript("js/jquery.flot.js", function() { load_other_scripts() });
134
+ </script>
@@ -0,0 +1,24 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2
+ <html xmlns="http://www.w3.org/1999/xhtml">
3
+ <head>
4
+ <meta http-equiv="Content-Type" content="text/html; charset=utf8" />
5
+ <title>Plant Watchdog</title>
6
+ </head>
7
+ <style>
8
+ #placeholder {
9
+ width: 350px;
10
+ height: 600px;
11
+ }
12
+
13
+ .loading {
14
+ background: url(/images/spinner.gif) no-repeat center center;
15
+ }
16
+ </style>
17
+ <body>
18
+
19
+ <iframe allowtransparency="true" id="plantwatchdog" name="plantwatchdog"
20
+ scrolling="no" frameborder="1" border="1"
21
+ style="border:1; width:400px;height:340px;margin:0;padding:0;"
22
+ src="graph.html?height=200px&width=320px"></iframe>
23
+ </body>
24
+ </html>
@@ -0,0 +1,41 @@
1
+ <form id="monthly_graph_form">
2
+ <label for="monthly_graph_year">Jahr:</label></td>
3
+ <select name="year" size="1" id="monthly_graph_year">
4
+ <% years = years_with_data(user);
5
+ years.each_index {
6
+ |i| %>
7
+ <option <%=i == years.size() -1 ? 'selected="selected"' : ""%>><%=years[i].to_s%></option>
8
+ <% } %>
9
+ </select>
10
+ <label for="monthly_graph_month">Monat:</label></td>
11
+ <select name="month" size="1" id="monthly_graph_month">
12
+ </select>
13
+
14
+ <input class="fetchSeries" type="submit" value="Zeichnen">
15
+
16
+ </form>
17
+ <div id="monthly_placeholder" style="width:<%=graph_width%>;height:<%=graph_height%>;"></div>
18
+
19
+ <script id="source" language="javascript" type="text/javascript">
20
+ $(function () {
21
+ var year = $('#monthly_graph_year');
22
+ var month = $('#monthly_graph_month');
23
+ year.selectChain({
24
+ target: month,
25
+ type: 'get',
26
+ data: {}
27
+ },
28
+ function(settings) { settings.url = '/availabledata/'+$('#monthly_graph_year').selectedOption();}
29
+ ).trigger('change');
30
+ month.change(function() {
31
+ if (year.get(0).options.selectedIndex >= 0) {
32
+ $('#monthly_graph_form:first').submit();
33
+ }
34
+ }) ;
35
+ });
36
+
37
+ var monthly = new Graph("#monthly_placeholder");
38
+ monthly.urlfunction = function() { return '/monthly/plant/' + $('#monthly_graph_year').selectedOption() + '/' + $('#monthly_graph_month').selectedOption() }
39
+ $("form#monthly_graph_form").submit(function() { return monthly.submitForm() } );
40
+
41
+ </script>
@@ -0,0 +1,161 @@
1
+ $:.unshift File.join(File.dirname(__FILE__),"..","lib")
2
+ $:.unshift File.join(File.dirname(__FILE__),"..")
3
+ require 'rubygems'
4
+ require 'plantwatchdog/model'
5
+ require 'plantwatchdog/aggregation'
6
+ require 'test/unit'
7
+ require 'test/test_base'
8
+
9
+ module PlantWatchdog
10
+ class ModelTest < Test::Unit::TestCase
11
+ include TestUtil
12
+ def test_device_aggregation
13
+ # make sure that the aggregation Methods are called correctly
14
+ # device
15
+ year = 2010
16
+ day = 50
17
+ plant = Model::Plant.new
18
+
19
+ inv_aggrules = { "agg1" => [:aggZeroParam], "agg2" => [:aggOneParam, :time], "agg3" => [:aggTwoParams, :time, :pac] }
20
+ inv_meta = [["time", "integer"], ["pac", "integer"]]
21
+ inverter1 = Model::Device.new
22
+ inverter1.aggrules = inv_aggrules
23
+ inverter1.plant = plant
24
+ inverter1.meta = inv_meta
25
+ inverter1.save!
26
+
27
+ invMeasurement = Model::MeasurementChunk.new()
28
+ invMeasurement.time_year = year
29
+ invMeasurement.time_day_of_year = day
30
+ invMeasurement.meta = inv_meta # TODO set automatically
31
+ invMeasurement.data = <<EOF
32
+ 12,33
33
+ 16,0
34
+ EOF
35
+ invMeasurement.device = inverter1
36
+ invMeasurement.save!
37
+
38
+ calledZeroParam = nil
39
+ calledOneParam = nil
40
+ calledTwoParams = nil
41
+
42
+ Aggregation::Methods.class.send(:define_method, :aggZeroParam, Proc.new { calledZeroParam = true })
43
+ Aggregation::Methods.class.send(:define_method, :aggOneParam, Proc.new { |p1| calledOneParam = p1 })
44
+ Aggregation::Methods.class.send(:define_method, :aggTwoParams, Proc.new { |p1,p2| calledTwoParams = [p1,p2] })
45
+
46
+ aggDevice = Aggregation::Device.create(inverter1, year, day)
47
+ aggDevice.aggregate
48
+
49
+ assert_equal(true, calledZeroParam)
50
+ assert_equal([12,16], calledOneParam)
51
+ assert_equal([[12,16], [33,0]], calledTwoParams)
52
+
53
+ Aggregation::Methods.class.send(:define_method, :nested, Proc.new { |t| t.first + t.last })
54
+ inverter1.aggrules = { "nested" => [:mult, 0.5, [:nested, :time]] }
55
+ device_aggregate = aggDevice.aggregate()
56
+ assert_equal(14, device_aggregate["nested"])
57
+
58
+ plant.aggrules = { "picked" => [ :pick, 0, :nested] }
59
+ plant.save!
60
+
61
+ aggPlant = Aggregation::Plant.new(plant, [ device_aggregate ])
62
+ assert_equal(14, aggPlant.aggregate()["picked"])
63
+ end
64
+
65
+ def test_aggregation
66
+ year = 2010
67
+ day = 50
68
+ plant = Model::Plant.new
69
+ plant.aggrules = { "eday" => [:sum, "eday"] }
70
+ plant.save
71
+
72
+ sunmeter = Model::Device.new
73
+ sunmeter.aggrules = { :avg_temperature => [:avg, :temperature], "irradiance" => [:integrate, :time, :irradiance] }
74
+ sunmeter.plant = plant
75
+ sunmeter.meta = [["time", "integer"], ["temperature", "integer"], ["irradiance", "integer"]]
76
+ sunmeter.save!
77
+
78
+ envMeasurement = Model::MeasurementChunk.new()
79
+ envMeasurement.time_year = year
80
+ envMeasurement.time_day_of_year = day
81
+ envMeasurement.data = <<EOF
82
+ 12,32,500
83
+ 15,30,480
84
+ EOF
85
+ envMeasurement.device = sunmeter
86
+ # TODO meta should be taken from sunmeter automatically
87
+ envMeasurement.meta = sunmeter.meta
88
+ envMeasurement.save!
89
+
90
+ # create two inverters with the same metadata
91
+ inv_aggrules = { "eday" => [:growth, :etotal], "pac" => [:integrate, :time, :pac], "expected" => [ :expected] }
92
+ inv_meta = [["time", "integer"], ["pac", "integer"], ["etotal", "float"]]
93
+ inverter1 = Model::Device.new
94
+ inverter1.aggrules = inv_aggrules
95
+ inverter1.plant = plant
96
+ inverter1.meta = inv_meta
97
+ inverter1.save!
98
+
99
+ invMeasurement = Model::MeasurementChunk.new()
100
+ invMeasurement.time_year = year
101
+ invMeasurement.time_day_of_year = day
102
+ invMeasurement.meta = inv_meta # TODO set automatically
103
+ invMeasurement.data = <<EOF
104
+ 12,33,60.1
105
+ 16,0,60.5
106
+ EOF
107
+ invMeasurement.device = inverter1
108
+ invMeasurement.save!
109
+
110
+ inverter2 = Model::Device.new
111
+ inverter2.aggrules = inv_aggrules
112
+ inverter2.plant = plant
113
+ inverter2.meta = inv_meta
114
+ inverter2.save!
115
+
116
+ invMeasurement = Model::MeasurementChunk.new()
117
+ invMeasurement.time_year = year
118
+ invMeasurement.time_day_of_year = day
119
+ invMeasurement.meta = inv_meta # TODO set automatically
120
+ invMeasurement.data = <<EOF
121
+ 12,43,90.1
122
+ 14,50,100.0
123
+ 18,35,105.0
124
+ EOF
125
+ invMeasurement.device = inverter2
126
+ invMeasurement.save!
127
+
128
+ runner = Aggregation::Runner.new
129
+ agg_sunmeter, agg_inv1, agg_inv2, agg_plant = runner.aggregate(Model::Plant.find(:first), year, day)
130
+ assert_equal(31, agg_sunmeter.data["avg_temperature"])
131
+ eday_inv1 = 60.5 - 60.1 ; eday_inv2 = 105.0 - 90.1;
132
+ pac_inv1 = 4 * 33 / 2.0
133
+ assert_equal(eday_inv1, agg_inv1.data["eday"])
134
+ assert_equal(pac_inv1, agg_inv1.data["pac"])
135
+ assert_equal(eday_inv2, agg_inv2.data["eday"])
136
+ assert_equal(eday_inv1 + eday_inv2, agg_plant.data["eday"])
137
+
138
+ # check that daily aggregate entry was created in db
139
+ saved_plant_agg = Model::MeasurementAggregate.find(:first, :conditions => ["plant_id=?", plant.id])
140
+ assert_equal(year, saved_plant_agg.time_year)
141
+ assert_equal(day, saved_plant_agg.time_day_of_year)
142
+ assert(saved_plant_agg.data.keys.include?("eday"))
143
+
144
+ end
145
+
146
+ def test_find_missing_aggregates
147
+ inverter = create_inverter
148
+
149
+ chunk = Model::MeasurementChunk.new()
150
+ chunk.time_year = 2222
151
+ chunk.time_day_of_year = 52
152
+ chunk.device = inverter
153
+ chunk.save!
154
+
155
+ runner = Aggregation::Runner.new
156
+ assert_equal([[chunk.time_year, chunk.time_day_of_year, inverter.plant.id]], runner.find_missing_aggregates)
157
+ runner.run
158
+ assert_equal([], runner.find_missing_aggregates)
159
+ end
160
+ end
161
+ end