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,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