smartermeter 0.1.0 → 0.2.0

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