smartermeter 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md ADDED
@@ -0,0 +1,4 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (January 8th, 2011)
4
+ * First public release
data/Gemfile CHANGED
@@ -1,2 +1,5 @@
1
1
  source :rubygems
2
2
  gemspec
3
+
4
+ # Used only in the Java client, which isn't distributed as a gem
5
+ gem "profligacy"
data/Rakefile CHANGED
@@ -86,7 +86,7 @@ namespace :rawr do
86
86
  dir = File.join(File.dirname(__FILE__), "vendor", "gems")
87
87
  FileUtils.rm_rf(dir)
88
88
  FileUtils.mkdir_p(dir)
89
- ["nokogiri", "mechanize", "crypt"].each do |gem|
89
+ ["nokogiri", "mechanize", "crypt", "profligacy"].each do |gem|
90
90
  `gem unpack -t "#{dir}" #{gem}`
91
91
  end
92
92
 
@@ -100,6 +100,9 @@ namespace :rawr do
100
100
  Dir.glob(File.join(dir, "nokogiri.old", "lib", "*")).each do |file|
101
101
  FileUtils.mv(file, File.join(dir))
102
102
  end
103
+ Dir.glob(File.join(dir, "profligacy.old", "lib", "*")).each do |file|
104
+ FileUtils.mv(file, File.join(dir))
105
+ end
103
106
  FileUtils.mv(File.join(dir, "crypt.old", "crypt"), File.join(dir, "crypt"))
104
107
  Dir.glob(File.join(dir, "mechanize.old", "lib", "*")).each do |file|
105
108
  FileUtils.mv(file, File.join(dir))
@@ -127,7 +130,7 @@ task :release => :build do
127
130
  sh "git tag v#{version}"
128
131
  sh "git push origin master"
129
132
  sh "git push origin v#{version}"
130
- #sh "gem push pkg/#{name}-#{version}.gem"
133
+ sh "gem push pkg/#{name}-#{version}.gem"
131
134
  end
132
135
 
133
136
  desc "Build #{gem_file} into the pkg directory"
data/bin/smartermeter CHANGED
@@ -7,4 +7,5 @@ rescue Exception
7
7
  end
8
8
  require 'smartermeter'
9
9
 
10
- SmarterMeter::Daemon.new.start
10
+ interface = SmarterMeter::Interfaces::CLI.new
11
+ SmarterMeter::Daemon.new(interface).start
@@ -1,11 +1,15 @@
1
+ require 'fileutils'
1
2
  require 'crypt/blowfish'
2
3
  require 'yaml'
3
- require 'logger'
4
4
  require 'date'
5
5
 
6
6
  module SmarterMeter
7
7
  class Daemon
8
8
 
9
+ def initialize(interface)
10
+ @ui = interface
11
+ end
12
+
9
13
  # Loads the configuration, and starts
10
14
  #
11
15
  # Never returns.
@@ -28,13 +32,6 @@ module SmarterMeter
28
32
  File.expand_path(File.join(@config[:data_dir], date.strftime("%Y-%m-%d.csv")))
29
33
  end
30
34
 
31
- def log
32
- return @logger if @logger
33
- @logger = Logger.new STDOUT
34
- @logger.level = Logger::INFO
35
- @logger
36
- end
37
-
38
35
  # Loads the configuration and prompts for required settings if they are
39
36
  # missing.
40
37
  #
@@ -49,7 +46,7 @@ module SmarterMeter
49
46
  # Returns the configuration hash.
50
47
  def load_configuration
51
48
  @config = {
52
- :start_date => Date.today,
49
+ :start_date => Date.today - 1,
53
50
  :data_dir => default_data_dir
54
51
  }
55
52
 
@@ -79,32 +76,22 @@ module SmarterMeter
79
76
  end
80
77
  end
81
78
 
79
+ # Returns true if the all of the required configuration has been set.
80
+ def has_configuration?
81
+ @config[:username] and @config[:password]
82
+ end
83
+
82
84
  # Prompts the user for required settings that are blank.
83
85
  #
84
86
  # Returns nothing.
85
87
  def verify_configuration
86
- return if @config[:username] and @config[:password]
87
-
88
- puts
89
- puts "Smartermeter: Initial Configuration"
90
- puts "--------------------------------------------------------------------------------"
91
- puts "This program stores your PG&E account username and password on disk. The"
92
- puts "password is encrypted but could be retrieved fairly easily. If this makes you"
93
- puts "uncomfortable quit now (use ctrl-c)."
94
- puts "--------------------------------------------------------------------------------"
95
-
96
- unless @config[:username]
97
- print "PG&E account username: "
98
- @config[:username] = gets.strip
99
- end
88
+ return if has_configuration?
100
89
 
101
- unless @config[:password]
102
- print "PG&E account password: "
103
- self.password = gets.strip
90
+ @ui.setup do |config|
91
+ @config.merge!(config)
92
+ self.password = config[:password] if config.has_key? :password
93
+ save_configuration
104
94
  end
105
-
106
- save_configuration
107
- puts "Setup complete"
108
95
  end
109
96
 
110
97
  # Saves the current configuration to disk.
@@ -124,13 +111,19 @@ module SmarterMeter
124
111
  one_hour = 60 * 60
125
112
 
126
113
  while true
114
+ unless has_configuration?
115
+ @ui.log.info("Waiting for configuration")
116
+ sleep(5)
117
+ next
118
+ end
119
+
127
120
  dates = dates_requiring_data
128
121
  unless dates.empty?
129
- log.info("Attempting to fetch data for: #{dates.join(",")}")
122
+ @ui.log.info("Attempting to fetch data for: #{dates.join(",")}")
130
123
  results = fetch_dates(dates)
131
- log.info("Successfully fetched: #{results.join(",")}")
124
+ @ui.log.info("Successfully fetched: #{results.join(",")}")
132
125
  else
133
- log.info("Sleeping")
126
+ @ui.log.info("Sleeping")
134
127
  end
135
128
  sleep(one_hour)
136
129
  end
@@ -141,32 +134,34 @@ module SmarterMeter
141
134
  # Note: An authorization failure will cause an exits, as it is a dire
142
135
  # condition.
143
136
  #
144
- # Returns a new Service instance which has been properly authorized.
137
+ # Returns a new Service instance which has been properly authorized and nil
138
+ # otherwise.
145
139
  def service
146
140
  service = Service.new
147
- log.info("Logging in as #{@config[:username]}")
148
- unless service.login(@config[:username], password)
149
- log.error("Incorrect username or password given.")
150
- log.error("Please remove ~/.smartermeter and configure smartermeter again.")
151
- exit(-1)
141
+ @ui.log.info("Logging in as #{@config[:username]}")
142
+ if service.login(@config[:username], password)
143
+ @ui.log.info("Logged in as #{@config[:username]}")
144
+ service
145
+ else
146
+ @ui.log.error("Login failed.")
147
+ @ui.log.error(service.last_page) if service.last_page
148
+ @ui.log.error(service.last_exception) if service.last_exception
149
+ @ui.log.error("If this happens repeatedly your login information may be incorrect")
150
+ @ui.log.error("Remove ~/.smartermeter and restart to re-configure smartermeter.")
151
+ nil
152
152
  end
153
- log.info("Logged in as #{@config[:username]}")
154
- service
155
153
  end
156
154
 
157
155
  # Connect and authenticate to the PG&E Website.
158
156
  #
159
157
  # It provides an instance of Service to the provided block
160
- # for direct manipulation.
158
+ # for direct manipulation. If there was a failure logging into the service
159
+ # the block will not be executed.
161
160
  #
162
161
  # Returns nothing.
163
162
  def connect
164
163
  s = service
165
- begin
166
- yield s
167
- rescue SocketError => e
168
- log.error("Could not access the PG&E site, are you connected to the Internet?")
169
- end
164
+ yield s if s
170
165
  end
171
166
 
172
167
  # Attempts to retrieve power data for each of the dates in the list.
@@ -179,25 +174,28 @@ module SmarterMeter
179
174
 
180
175
  connect do |service|
181
176
  dates.each do |date|
182
- log.info("Fetching #{date}")
177
+ @ui.log.info("Fetching #{date}")
178
+
183
179
  data = service.fetch_csv(date)
180
+ next if data.empty?
184
181
 
185
- log.info("Verifying #{date}")
182
+ @ui.log.info("Verifying #{date}")
186
183
  samples = Sample.parse_csv(data).values.first
187
184
  first_sample = samples.first
188
185
 
189
186
  if first_sample.kwh
190
- log.info("Saving #{date}")
187
+ @ui.log.info("Saving #{date}")
188
+ FileUtils.mkdir_p(File.dirname(data_file(date)))
191
189
  File.open(data_file(date), "w") do |f|
192
190
  f.write(data)
193
191
  end
194
192
 
195
193
  upload(date, samples)
196
194
 
197
- log.info("Completed #{date}")
195
+ @ui.log.info("Completed #{date}")
198
196
  completed << date
199
197
  else
200
- log.info("Incomplete #{date}")
198
+ @ui.log.info("Incomplete #{date}")
201
199
  end
202
200
  end
203
201
  end
@@ -208,12 +206,12 @@ module SmarterMeter
208
206
  def upload(date, samples)
209
207
  case @config[:transport]
210
208
  when :google_powermeter
211
- log.info("Uploading #{date} to Google PowerMeter")
209
+ @ui.log.info("Uploading #{date} to Google PowerMeter")
212
210
  transport = SmarterMeter::Transports::GooglePowerMeter.new(@config[:google_powermeter])
213
211
  if transport.upload(samples)
214
- log.info("Upload for #{date} complete")
212
+ @ui.log.info("Upload for #{date} complete")
215
213
  else
216
- log.info("Upload for #{date} failed")
214
+ @ui.log.info("Upload for #{date} failed")
217
215
  end
218
216
  end
219
217
  end
@@ -0,0 +1,41 @@
1
+ require 'logger'
2
+
3
+ module SmarterMeter
4
+ module Interfaces
5
+ class CLI
6
+ # Returns a logger like interface to log errors and warnings to.
7
+ def log
8
+ return @logger if @logger
9
+ @logger = Logger.new STDOUT
10
+ @logger.level = Logger::INFO
11
+ @logger
12
+ end
13
+
14
+ # Public: Called when ~/.smartermeter needs to be configured.
15
+ # Yields a hash containing the configuration by the user.
16
+ #
17
+ # Returns nothing
18
+ def setup
19
+ puts
20
+ puts "Smartermeter: Initial Configuration"
21
+ puts "--------------------------------------------------------------------------------"
22
+ puts "This program stores your PG&E account username and password on disk. The"
23
+ puts "password is encrypted but could be retrieved fairly easily. If this makes you"
24
+ puts "uncomfortable quit now (use ctrl-c)."
25
+ puts "--------------------------------------------------------------------------------"
26
+
27
+ config = {}
28
+
29
+ print "PG&E account username: "
30
+ config[:username] = gets.strip
31
+
32
+ print "PG&E account password: "
33
+ config[:password] = gets.strip
34
+
35
+ puts "Configuration finished"
36
+
37
+ yield config
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,85 @@
1
+ require 'logger'
2
+ require 'profligacy/swing'
3
+ require 'profligacy/lel'
4
+
5
+ module SmarterMeter
6
+ module Interfaces
7
+ class Swing
8
+ include_package "java.awt"
9
+
10
+ def initialize
11
+ # TODO: Implement a way to update the settings
12
+ #settings_item = MenuItem.new("Settings")
13
+ #settings_item.add_action_listener { SettingsWindow.new do |config|
14
+ # puts config.inspect
15
+ # end
16
+ #}
17
+
18
+ exit_item = MenuItem.new("Exit")
19
+ exit_item.add_action_listener {java.lang.System::exit(0)}
20
+
21
+ popup = PopupMenu.new
22
+ #popup.add(settings_item)
23
+ popup.add(exit_item)
24
+
25
+ image = Toolkit::default_toolkit.get_image("icon.png")
26
+ tray_icon = TrayIcon.new(image, "Smartermeter", popup)
27
+ tray_icon.image_auto_size = true
28
+
29
+ tray = SystemTray::system_tray
30
+ tray.add(tray_icon)
31
+ end
32
+
33
+ # Returns a logger like interface to log errors and warnings to.
34
+ def log
35
+ return @logger if @logger
36
+ @logger = Logger.new(File.expand_path("~/.smartermeter.log"))
37
+ @logger.level = Logger::INFO
38
+ @logger
39
+ end
40
+
41
+ # Public: Called when ~/.smartermeter needs to be configured.
42
+ # Yields a hash containing the configuration specified by the user.
43
+ #
44
+ # Returns nothing.
45
+ def setup
46
+ SettingsWindow.new do |config|
47
+ yield config
48
+ end
49
+ end
50
+ end
51
+
52
+ class SettingsWindow
53
+ include_package "javax.swing"
54
+
55
+ def initialize(&block)
56
+ layout = "
57
+ [ username_label | (150)username_field ]
58
+ [ password_label | (150)password_field ]
59
+ [ _ | >save_button ]
60
+ "
61
+
62
+ @ui = Profligacy::Swing::LEL.new(JFrame, layout) do |c,i|
63
+ c.username_label = JLabel.new "PG&E Username:"
64
+ c.username_field = JTextField.new
65
+ c.password_label = JLabel.new "PG&E Password:"
66
+ c.password_field = JPasswordField.new
67
+ c.save_button = JButton.new("Save")
68
+
69
+ i.save_button = { :action => proc do |t, e|
70
+ config = {
71
+ :username => @ui.username_field.text,
72
+ :password => @ui.password_field.text
73
+ }
74
+ @frame.dispose
75
+ yield config
76
+ end
77
+ }
78
+ end
79
+
80
+ @frame = @ui.build(:args => "Smartermeter Settings")
81
+ @frame.defaultCloseOperation = JFrame::DISPOSE_ON_CLOSE
82
+ end
83
+ end
84
+ end
85
+ end
@@ -5,5 +5,7 @@ require 'jruby'
5
5
  JRuby.objectspace=true
6
6
 
7
7
  require 'smartermeter'
8
+ require 'smartermeter/interfaces/swing'
8
9
 
9
- SmarterMeter::Daemon.new.start
10
+ interface = SmarterMeter::Interfaces::Swing.new
11
+ SmarterMeter::Daemon.new(interface).start
@@ -7,6 +7,9 @@ module SmarterMeter
7
7
  OVERVIEW_URL = "https://www.pge.com/csol/actions/login.do?aw"
8
8
  ENERGYGUIDE_AUTH_URL = "https://www.energyguide.com/LoadAnalysis/LoadAnalysis.aspx?Referrerid=154"
9
9
 
10
+ attr_reader :last_page
11
+ attr_reader :last_exception
12
+
10
13
  def initialize
11
14
  @agent = WWW::Mechanize.new { |agent|
12
15
  agent.user_agent_alias = 'Mac Safari'
@@ -15,64 +18,79 @@ module SmarterMeter
15
18
 
16
19
  # Returns true upon succesful login and false otherwise
17
20
  def login(username, password)
18
- @agent.get(LOGIN_URL) do |page|
19
- logged_in_page = page.form_with(:action => 'https://www.pge.com/eum/login') do |login|
20
- login.USER = username
21
- login.PASSWORD = password
22
- end.submit
21
+ begin
22
+ @agent.get(LOGIN_URL) do |page|
23
+ logged_in_page = page.form_with(:action => 'https://www.pge.com/eum/login') do |login|
24
+ login.USER = username
25
+ login.PASSWORD = password
26
+ end.submit
27
+ end
28
+
29
+ # There is a crazy meta-reload thing here that mechanize doesn't handle
30
+ # correctly by itself so let's help it along...
31
+ @agent.get(OVERVIEW_URL) do |page|
32
+
33
+ return false if page.title =~ /PG&E Login/
34
+
35
+ # Load the PG&E Terms of Use page
36
+ tou_link = page.link_with(:href => '/csol/actions/billingDisclaimer.do?actionType=hourly')
37
+ unless tou_link
38
+ @last_page = page
39
+ return false
40
+ end
41
+ tou_page = @agent.click(tou_link)
42
+ form = tou_page.forms().first
43
+ agree_button = form.button_with(:value => 'I Understand - Proceed')
44
+
45
+ # Agree to the terms of use
46
+ form['agreement'] = 'yes'
47
+
48
+ # Load up the PG&E frame page for historical data
49
+ hourly_usage_container = form.submit(agree_button)
50
+
51
+ # Now load up the frame with the content
52
+ hourly_usage = @agent.click(hourly_usage_container.frames.select{|f| f.href == "/csol/nexus/content.jsp"}.first)
53
+
54
+ # Now post the authentication information from PG&E to energyguide.com
55
+ @data_page = hourly_usage.form_with(:action => ENERGYGUIDE_AUTH_URL).submit
56
+ end
57
+ @authenticated = true
58
+ rescue Exception => e
59
+ @last_exception = e
60
+ return false
23
61
  end
24
-
25
- # There is a crazy meta-reload thing here that mechanize doesn't handle
26
- # correctly by itself so let's help it along...
27
- @agent.get(OVERVIEW_URL) do |page|
28
-
29
- return false if page.title =~ /PG&E Login/
30
-
31
- # Load the PG&E Terms of Use page
32
- tou_page = @agent.click(page.link_with(:href => '/csol/actions/billingDisclaimer.do?actionType=hourly'))
33
- form = tou_page.forms().first
34
- agree_button = form.button_with(:value => 'I Understand - Proceed')
35
-
36
- # Agree to the terms of use
37
- form['agreement'] = 'yes'
38
-
39
- # Load up the PG&E frame page for historical data
40
- hourly_usage_container = form.submit(agree_button)
41
-
42
- # Now load up the frame with the content
43
- hourly_usage = @agent.click(hourly_usage_container.frames.select{|f| f.href == "/csol/nexus/content.jsp"}.first)
44
-
45
- # Now post the authentication information from PG&E to energyguide.com
46
- @data_page = hourly_usage.form_with(:action => ENERGYGUIDE_AUTH_URL).submit
47
- end
48
- true
49
62
  end
50
63
 
51
64
  def fetch_csv(date)
52
- # TODO: Check if the authentication has been called
65
+ raise RuntimeException, "login must be called before fetch_csv" unless @authenticated
53
66
 
54
67
  # Now we almost actually have data. However we need to setup the desired
55
68
  # parameters first before we can get the exportable data. This really shouldn't
56
69
  # be necessary.
57
- hourly_data = @data_page.form_with(:action => "/LoadAnalysis/LoadAnalysis.aspx") do |form|
58
- form['__EVENTTARGET'] = "objChartSelect$butSubmit"
59
- form['objTimePeriods$objExport$hidChart'] = "Hourly Usage"
60
- form['objTimePeriods$objExport$hidChartID'] = 8
61
- form['objChartSelect$ddChart'] = 8 # Hourly usage
62
-
63
- form['objTimePeriods$objExport$hidTimePeriod'] = "Week"
64
- form['objTimePeriods$objExport$hidTimePeriodID'] = 3
65
- form['objTimePeriods$rlPeriod'] = 3
66
-
67
- form['objChartSelect$ccSelectedDate1'] = date.strftime("%m/%d/%Y")
68
- end.submit
70
+ begin
71
+ hourly_data = @data_page.form_with(:action => "/LoadAnalysis/LoadAnalysis.aspx") do |form|
72
+ form['__EVENTTARGET'] = "objChartSelect$butSubmit"
73
+ form['objTimePeriods$objExport$hidChart'] = "Hourly Usage"
74
+ form['objTimePeriods$objExport$hidChartID'] = 8
75
+ form['objChartSelect$ddChart'] = 8 # Hourly usage
76
+
77
+ form['objTimePeriods$objExport$hidTimePeriod'] = "Week"
78
+ form['objTimePeriods$objExport$hidTimePeriodID'] = 3
79
+ form['objTimePeriods$rlPeriod'] = 3
80
+
81
+ form['objChartSelect$ccSelectedDate1'] = date.strftime("%m/%d/%Y")
82
+ end.submit
69
83
 
70
- # Now the beautiful data...
71
- hourly_csv = hourly_data.form_with(:action => "/LoadAnalysis/LoadAnalysis.aspx") do |form|
72
- form['__EVENTTARGET'] = "objTimePeriods$objExport$butExport"
73
- end.submit
84
+ # Now the beautiful data...
85
+ hourly_csv = hourly_data.form_with(:action => "/LoadAnalysis/LoadAnalysis.aspx") do |form|
86
+ form['__EVENTTARGET'] = "objTimePeriods$objExport$butExport"
87
+ end.submit
74
88
 
75
- hourly_csv.body
89
+ hourly_csv.body
90
+ rescue Timeout::Error => e
91
+ @last_exception = e
92
+ return ""
93
+ end
76
94
  end
77
95
  end
78
96
  end
data/lib/smartermeter.rb CHANGED
@@ -2,7 +2,8 @@ require 'smartermeter/sample'
2
2
  require 'smartermeter/service'
3
3
  require 'smartermeter/daemon'
4
4
  require 'smartermeter/transports/google_powermeter'
5
+ require 'smartermeter/interfaces/cli'
5
6
 
6
7
  module SmarterMeter
7
- VERSION = "0.1.0"
8
+ VERSION = "0.2.0"
8
9
  end
data/smartermeter.gemspec CHANGED
@@ -13,8 +13,8 @@ Gem::Specification.new do |s|
13
13
  ## If your rubyforge_project name is different, then edit it and comment out
14
14
  ## the sub! line in the Rakefile
15
15
  s.name = 'smartermeter'
16
- s.version = '0.1.0'
17
- s.date = '2011-01-08'
16
+ s.version = '0.2.0'
17
+ s.date = '2011-01-25'
18
18
  s.rubyforge_project = 'smartermeter'
19
19
 
20
20
  ## Make sure your summary is short. The description may be as long
@@ -61,6 +61,7 @@ Gem::Specification.new do |s|
61
61
  ## THE MANIFEST COMMENTS, they are used as delimiters by the task.
62
62
  # = MANIFEST =
63
63
  s.files = %w[
64
+ CHANGELOG.md
64
65
  Gemfile
65
66
  README.md
66
67
  Rakefile
@@ -68,6 +69,8 @@ Gem::Specification.new do |s|
68
69
  build_configuration.rb
69
70
  lib/smartermeter.rb
70
71
  lib/smartermeter/daemon.rb
72
+ lib/smartermeter/interfaces/cli.rb
73
+ lib/smartermeter/interfaces/swing.rb
71
74
  lib/smartermeter/main.rb
72
75
  lib/smartermeter/sample.rb
73
76
  lib/smartermeter/service.rb
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 1
7
+ - 2
8
8
  - 0
9
- version: 0.1.0
9
+ version: 0.2.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Matt Colyer
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2011-01-08 00:00:00 -08:00
17
+ date: 2011-01-25 00:00:00 -08:00
18
18
  default_executable: smartermeter
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -82,6 +82,7 @@ extensions: []
82
82
  extra_rdoc_files: []
83
83
 
84
84
  files:
85
+ - CHANGELOG.md
85
86
  - Gemfile
86
87
  - README.md
87
88
  - Rakefile
@@ -89,6 +90,8 @@ files:
89
90
  - build_configuration.rb
90
91
  - lib/smartermeter.rb
91
92
  - lib/smartermeter/daemon.rb
93
+ - lib/smartermeter/interfaces/cli.rb
94
+ - lib/smartermeter/interfaces/swing.rb
92
95
  - lib/smartermeter/main.rb
93
96
  - lib/smartermeter/sample.rb
94
97
  - lib/smartermeter/service.rb