smartermeter 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +2 -0
- data/README.md +65 -0
- data/Rakefile +182 -0
- data/bin/smartermeter +10 -0
- data/build_configuration.rb +89 -0
- data/lib/smartermeter/daemon.rb +236 -0
- data/lib/smartermeter/main.rb +9 -0
- data/lib/smartermeter/sample.rb +68 -0
- data/lib/smartermeter/service.rb +78 -0
- data/lib/smartermeter/transports/cacert.pem +3509 -0
- data/lib/smartermeter/transports/google_powermeter.erb +16 -0
- data/lib/smartermeter/transports/google_powermeter.rb +42 -0
- data/lib/smartermeter.rb +8 -0
- data/smartermeter.gemspec +90 -0
- data/specs/fixtures/data.csv +21 -0
- data/specs/fixtures/expected_google_request.xml +292 -0
- data/specs/sample_spec.rb +18 -0
- data/specs/service_spec.rb +8 -0
- data/specs/spec_helper.rb +9 -0
- data/specs/transports/google_powermeter_spec.rb +19 -0
- metadata +136 -0
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
Smartermeter - the smarter way to read your PGE SmartMeter
|
2
|
+
=========================================================
|
3
|
+
|
4
|
+
So I have PGE SmartMeter and I like playing with data. However I didn't really
|
5
|
+
want to jump through 37 hoops to see the data on PG&E's website. So I made
|
6
|
+
this.
|
7
|
+
|
8
|
+
While making this library I discovered that PG&E doesn't even manage the
|
9
|
+
software for the energy reporting. It's all done by energyguide.com. Not
|
10
|
+
terribly useful but an interesting piece of trivia.
|
11
|
+
|
12
|
+
Getting Started
|
13
|
+
---------------
|
14
|
+
|
15
|
+
gem install smartermeter
|
16
|
+
smartermeter
|
17
|
+
|
18
|
+
Google PowerMeter
|
19
|
+
-----------------
|
20
|
+
|
21
|
+
Once you've configured smartermeter once, you might want to use it with Google
|
22
|
+
PowerMeter.
|
23
|
+
|
24
|
+
1. Visit: https://www.google.com/powermeter/device/activate?mfg=Ruby&model=SmarterMeter&did=PGE&dvars=1
|
25
|
+
1. Then sign in with your desired Google Account.
|
26
|
+
1. Follow the directions on screen.
|
27
|
+
1. On the final screen copy the entire "authInfo" into your favorite editor.
|
28
|
+
Pull out the "token" and the "path" from the string.
|
29
|
+
1. Take the "path" you collected previously and append ".d1" to the end of it.
|
30
|
+
1. Then append the following to your ~/.smartermeter file to
|
31
|
+
automatically upload data as it's retrieved from PG&E.
|
32
|
+
|
33
|
+
:transport: :google_powermeter
|
34
|
+
:google_powermeter:
|
35
|
+
:token: "your-token"
|
36
|
+
:variable: "your-path-with.d1-appended"
|
37
|
+
|
38
|
+
To Build
|
39
|
+
--------
|
40
|
+
|
41
|
+
In order to build the self contained binaries, you'll need a working jruby interpreter.
|
42
|
+
|
43
|
+
git clone git://github.com/mcolyer/smartermeter.git
|
44
|
+
cd smartermeter
|
45
|
+
rake rawr:prepare
|
46
|
+
rake rawr:bundle:exe # builds a windows executable
|
47
|
+
rake rawr:bundle:app # builds an OSX executable
|
48
|
+
rake build # builds a ruby gem
|
49
|
+
|
50
|
+
Questions
|
51
|
+
---------
|
52
|
+
|
53
|
+
* How much lag is there?
|
54
|
+
|
55
|
+
It'll show you the last full day's worth of data. The PGE website claims that
|
56
|
+
data becomes available around 3-10pm on the following day. However my
|
57
|
+
experience says that it's sometimes available earlier.
|
58
|
+
|
59
|
+
* How long is data saved for?
|
60
|
+
|
61
|
+
I don't know. If you know tell me.
|
62
|
+
|
63
|
+
* How can I help?
|
64
|
+
|
65
|
+
Make sure it works, make cool things with it or send me git pull requests.
|
data/Rakefile
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'date'
|
4
|
+
|
5
|
+
#############################################################################
|
6
|
+
#
|
7
|
+
# Helper functions
|
8
|
+
#
|
9
|
+
#############################################################################
|
10
|
+
|
11
|
+
def name
|
12
|
+
@name ||= Dir['*.gemspec'].first.split('.').first
|
13
|
+
end
|
14
|
+
|
15
|
+
def version
|
16
|
+
line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/]
|
17
|
+
line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
|
18
|
+
end
|
19
|
+
|
20
|
+
def date
|
21
|
+
Date.today.to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
def rubyforge_project
|
25
|
+
name
|
26
|
+
end
|
27
|
+
|
28
|
+
def gemspec_file
|
29
|
+
"#{name}.gemspec"
|
30
|
+
end
|
31
|
+
|
32
|
+
def gem_file
|
33
|
+
"#{name}-#{version}.gem"
|
34
|
+
end
|
35
|
+
|
36
|
+
def replace_header(head, header_name)
|
37
|
+
head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"}
|
38
|
+
end
|
39
|
+
|
40
|
+
#############################################################################
|
41
|
+
#
|
42
|
+
# Standard tasks
|
43
|
+
#
|
44
|
+
#############################################################################
|
45
|
+
|
46
|
+
task :default => :test
|
47
|
+
|
48
|
+
require 'rake/testtask'
|
49
|
+
Rake::TestTask.new(:test) do |test|
|
50
|
+
test.libs << 'lib' << 'test'
|
51
|
+
test.pattern = 'specs/*_spec.rb'
|
52
|
+
test.verbose = true
|
53
|
+
end
|
54
|
+
|
55
|
+
desc "Generate RCov test coverage and open in your browser"
|
56
|
+
task :coverage do
|
57
|
+
require 'rcov'
|
58
|
+
sh "rm -fr coverage"
|
59
|
+
sh "rcov test/test_*.rb"
|
60
|
+
sh "open coverage/index.html"
|
61
|
+
end
|
62
|
+
|
63
|
+
require 'rake/rdoctask'
|
64
|
+
Rake::RDocTask.new do |rdoc|
|
65
|
+
rdoc.rdoc_dir = 'rdoc'
|
66
|
+
rdoc.title = "#{name} #{version}"
|
67
|
+
rdoc.rdoc_files.include('README*')
|
68
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
69
|
+
end
|
70
|
+
|
71
|
+
desc "Open an irb session preloaded with this library"
|
72
|
+
task :console do
|
73
|
+
sh "irb -rubygems -r ./lib/#{name}.rb"
|
74
|
+
end
|
75
|
+
|
76
|
+
#############################################################################
|
77
|
+
#
|
78
|
+
# Custom tasks (add your own tasks here)
|
79
|
+
#
|
80
|
+
#############################################################################
|
81
|
+
|
82
|
+
require 'rawr'
|
83
|
+
|
84
|
+
namespace :rawr do
|
85
|
+
task :prepare do
|
86
|
+
dir = File.join(File.dirname(__FILE__), "vendor", "gems")
|
87
|
+
FileUtils.rm_rf(dir)
|
88
|
+
FileUtils.mkdir_p(dir)
|
89
|
+
["nokogiri", "mechanize", "crypt"].each do |gem|
|
90
|
+
`gem unpack -t "#{dir}" #{gem}`
|
91
|
+
end
|
92
|
+
|
93
|
+
# Rawr can't handle folders with dashes in the name, so we'll remove the
|
94
|
+
# version numbers from the gems.
|
95
|
+
Dir.glob(File.join(dir, "*-*")).each do |gem|
|
96
|
+
no_version = File.basename(gem).split("-")[0] + ".old"
|
97
|
+
FileUtils.mv(gem, File.join(dir, no_version))
|
98
|
+
end
|
99
|
+
|
100
|
+
Dir.glob(File.join(dir, "nokogiri.old", "lib", "*")).each do |file|
|
101
|
+
FileUtils.mv(file, File.join(dir))
|
102
|
+
end
|
103
|
+
FileUtils.mv(File.join(dir, "crypt.old", "crypt"), File.join(dir, "crypt"))
|
104
|
+
Dir.glob(File.join(dir, "mechanize.old", "lib", "*")).each do |file|
|
105
|
+
FileUtils.mv(file, File.join(dir))
|
106
|
+
end
|
107
|
+
|
108
|
+
Dir.glob(File.join(dir, "*.old")).each do |gem|
|
109
|
+
FileUtils.rm_rf(gem)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
#############################################################################
|
115
|
+
#
|
116
|
+
# Packaging tasks
|
117
|
+
#
|
118
|
+
#############################################################################
|
119
|
+
|
120
|
+
desc "Create tag v#{version} and build and push #{gem_file} to Rubygems"
|
121
|
+
task :release => :build do
|
122
|
+
unless `git branch` =~ /^\* master$/
|
123
|
+
puts "You must be on the master branch to release!"
|
124
|
+
exit!
|
125
|
+
end
|
126
|
+
sh "git commit --allow-empty -a -m 'Release #{version}'"
|
127
|
+
sh "git tag v#{version}"
|
128
|
+
sh "git push origin master"
|
129
|
+
sh "git push origin v#{version}"
|
130
|
+
#sh "gem push pkg/#{name}-#{version}.gem"
|
131
|
+
end
|
132
|
+
|
133
|
+
desc "Build #{gem_file} into the pkg directory"
|
134
|
+
task :build => :gemspec do
|
135
|
+
sh "mkdir -p pkg"
|
136
|
+
sh "gem build #{gemspec_file}"
|
137
|
+
sh "mv #{gem_file} pkg"
|
138
|
+
end
|
139
|
+
|
140
|
+
desc "Generate #{gemspec_file}"
|
141
|
+
task :gemspec => :validate do
|
142
|
+
# read spec file and split out manifest section
|
143
|
+
spec = File.read(gemspec_file)
|
144
|
+
head, manifest, tail = spec.split(" # = MANIFEST =\n")
|
145
|
+
|
146
|
+
# replace name version and date
|
147
|
+
replace_header(head, :name)
|
148
|
+
replace_header(head, :version)
|
149
|
+
replace_header(head, :date)
|
150
|
+
#comment this out if your rubyforge_project has a different name
|
151
|
+
replace_header(head, :rubyforge_project)
|
152
|
+
|
153
|
+
# determine file list from git ls-files
|
154
|
+
files = `git ls-files`.
|
155
|
+
split("\n").
|
156
|
+
sort.
|
157
|
+
reject { |file| file =~ /^\./ }.
|
158
|
+
reject { |file| file =~ /^(rdoc|pkg)/ }.
|
159
|
+
reject { |file| file =~ /^rawr/ }.
|
160
|
+
reject { |file| file =~ /\.jar$/ }.
|
161
|
+
map { |file| " #{file}" }.
|
162
|
+
join("\n")
|
163
|
+
|
164
|
+
# piece file back together and write
|
165
|
+
manifest = " s.files = %w[\n#{files}\n ]\n"
|
166
|
+
spec = [head, manifest, tail].join(" # = MANIFEST =\n")
|
167
|
+
File.open(gemspec_file, 'w') { |io| io.write(spec) }
|
168
|
+
puts "Updated #{gemspec_file}"
|
169
|
+
end
|
170
|
+
|
171
|
+
desc "Validate #{gemspec_file}"
|
172
|
+
task :validate do
|
173
|
+
libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"]
|
174
|
+
unless libfiles.empty?
|
175
|
+
puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir."
|
176
|
+
exit!
|
177
|
+
end
|
178
|
+
unless Dir['VERSION*'].empty?
|
179
|
+
puts "A `VERSION` file at root level violates Gem best practices."
|
180
|
+
exit!
|
181
|
+
end
|
182
|
+
end
|
data/bin/smartermeter
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
configuration do |c|
|
2
|
+
# The name for your resulting application file (e.g., if the project_name is 'foo' then you'll get foo.jar, foo.exe, etc.)
|
3
|
+
# default value: "smartermeter"
|
4
|
+
#
|
5
|
+
#c.project_name = "smartermeter"
|
6
|
+
|
7
|
+
# Undocumented option 'output_dir'
|
8
|
+
# default value: "package"
|
9
|
+
#
|
10
|
+
c.output_dir = "pkg"
|
11
|
+
|
12
|
+
# The main ruby file to invoke, minus the .rb extension
|
13
|
+
# default value: "main"
|
14
|
+
#
|
15
|
+
c.main_ruby_file = "smartermeter/main"
|
16
|
+
|
17
|
+
# The fully-qualified name of the main Java file used to initiate the application.
|
18
|
+
# default value: "org.rubyforge.rawr.Main"
|
19
|
+
#
|
20
|
+
#c.main_java_file = "org.rubyforge.rawr.Main"
|
21
|
+
|
22
|
+
# A list of directories where source files reside
|
23
|
+
# default value: ["src"]
|
24
|
+
#
|
25
|
+
c.source_dirs = ["lib", "vendor/gems", "rawr"]
|
26
|
+
|
27
|
+
# A list of regexps of files to exclude
|
28
|
+
# default value: []
|
29
|
+
#
|
30
|
+
#c.source_exclude_filter = []
|
31
|
+
|
32
|
+
# Whether Ruby source files should be compiled into .class files
|
33
|
+
# default value: true
|
34
|
+
#
|
35
|
+
#c.compile_ruby_files = true
|
36
|
+
|
37
|
+
# A list of individual Java library files to include.
|
38
|
+
# default value: []
|
39
|
+
#
|
40
|
+
#c.java_lib_files = []
|
41
|
+
|
42
|
+
# A list of directories for rawr to include . All files in the given directories get bundled up.
|
43
|
+
# default value: ["lib/java"]
|
44
|
+
#
|
45
|
+
c.java_lib_dirs = ["vendor/java"]
|
46
|
+
|
47
|
+
# Undocumented option 'files_to_copy'
|
48
|
+
# default value: []
|
49
|
+
#
|
50
|
+
#c.files_to_copy = []
|
51
|
+
|
52
|
+
# Undocumented option 'target_jvm_version'
|
53
|
+
# default value: 1.6
|
54
|
+
#
|
55
|
+
#c.target_jvm_version = 1.6
|
56
|
+
|
57
|
+
# Undocumented option 'jvm_arguments'
|
58
|
+
# default value: ""
|
59
|
+
#
|
60
|
+
#c.jvm_arguments = ""
|
61
|
+
|
62
|
+
# Undocumented option 'java_library_path'
|
63
|
+
# default value: ""
|
64
|
+
#
|
65
|
+
#c.java_library_path = ""
|
66
|
+
|
67
|
+
# Undocumented option 'extra_user_jars'
|
68
|
+
# default value: {}
|
69
|
+
#
|
70
|
+
#c.extra_user_jars[:data] = { :directory => 'data/images/png',
|
71
|
+
# :location_in_jar => 'images',
|
72
|
+
# :exclude => /*.bak$/ }
|
73
|
+
|
74
|
+
# Undocumented option 'mac_do_not_generate_plist'
|
75
|
+
# default value: nil
|
76
|
+
#
|
77
|
+
#c.mac_do_not_generate_plist = nil
|
78
|
+
|
79
|
+
# Undocumented option 'mac_icon_path'
|
80
|
+
# default value: nil
|
81
|
+
#
|
82
|
+
#c.mac_icon_path = nil
|
83
|
+
|
84
|
+
# Undocumented option 'windows_icon_path'
|
85
|
+
# default value: nil
|
86
|
+
#
|
87
|
+
#c.windows_icon_path = nil
|
88
|
+
|
89
|
+
end
|
@@ -0,0 +1,236 @@
|
|
1
|
+
require 'crypt/blowfish'
|
2
|
+
require 'yaml'
|
3
|
+
require 'logger'
|
4
|
+
require 'date'
|
5
|
+
|
6
|
+
module SmarterMeter
|
7
|
+
class Daemon
|
8
|
+
|
9
|
+
# Loads the configuration, and starts
|
10
|
+
#
|
11
|
+
# Never returns.
|
12
|
+
def start
|
13
|
+
configure
|
14
|
+
run
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
def config_file
|
19
|
+
File.expand_path("~/.smartermeter")
|
20
|
+
end
|
21
|
+
|
22
|
+
def default_data_dir
|
23
|
+
File.expand_path(File.join(File.dirname(__FILE__), "..", "data"))
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns a filename for the data belonging to the given date.
|
27
|
+
def data_file(date)
|
28
|
+
File.expand_path(File.join(@config[:data_dir], date.strftime("%Y-%m-%d.csv")))
|
29
|
+
end
|
30
|
+
|
31
|
+
def log
|
32
|
+
return @logger if @logger
|
33
|
+
@logger = Logger.new STDOUT
|
34
|
+
@logger.level = Logger::INFO
|
35
|
+
@logger
|
36
|
+
end
|
37
|
+
|
38
|
+
# Loads the configuration and prompts for required settings if they are
|
39
|
+
# missing.
|
40
|
+
#
|
41
|
+
# Returns nothing.
|
42
|
+
def configure
|
43
|
+
load_configuration
|
44
|
+
verify_configuration
|
45
|
+
end
|
46
|
+
|
47
|
+
# Loads the configuration from disk.
|
48
|
+
#
|
49
|
+
# Returns the configuration hash.
|
50
|
+
def load_configuration
|
51
|
+
@config = {
|
52
|
+
:start_date => Date.today,
|
53
|
+
:data_dir => default_data_dir
|
54
|
+
}
|
55
|
+
|
56
|
+
if File.exist?(config_file)
|
57
|
+
@config = YAML.load_file(config_file)
|
58
|
+
end
|
59
|
+
|
60
|
+
@config
|
61
|
+
end
|
62
|
+
|
63
|
+
def cipher
|
64
|
+
Crypt::Blowfish.new("Our easily discoverable key.")
|
65
|
+
end
|
66
|
+
|
67
|
+
# Takes the unencrypted password and encrypts it.
|
68
|
+
def password=(unencrypted)
|
69
|
+
@config[:password] = cipher.encrypt_block(unencrypted)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns the clear-text password or nil if it isn't set.
|
73
|
+
def password
|
74
|
+
password = @config.fetch(:password, nil)
|
75
|
+
if password
|
76
|
+
cipher.decrypt_block(password)
|
77
|
+
else
|
78
|
+
password
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Prompts the user for required settings that are blank.
|
83
|
+
#
|
84
|
+
# Returns nothing.
|
85
|
+
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
|
100
|
+
|
101
|
+
unless @config[:password]
|
102
|
+
print "PG&E account password: "
|
103
|
+
self.password = gets.strip
|
104
|
+
end
|
105
|
+
|
106
|
+
save_configuration
|
107
|
+
puts "Setup complete"
|
108
|
+
end
|
109
|
+
|
110
|
+
# Saves the current configuration to disk.
|
111
|
+
#
|
112
|
+
# Returns nothing.
|
113
|
+
def save_configuration
|
114
|
+
File.open(config_file, "w") do |file|
|
115
|
+
file.write(YAML.dump(@config))
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Continually checks for new data for any missing days, since the first day
|
120
|
+
# smartermeter started watching.
|
121
|
+
#
|
122
|
+
# Never returns.
|
123
|
+
def run
|
124
|
+
one_hour = 60 * 60
|
125
|
+
|
126
|
+
while true
|
127
|
+
dates = dates_requiring_data
|
128
|
+
unless dates.empty?
|
129
|
+
log.info("Attempting to fetch data for: #{dates.join(",")}")
|
130
|
+
results = fetch_dates(dates)
|
131
|
+
log.info("Successfully fetched: #{results.join(",")}")
|
132
|
+
else
|
133
|
+
log.info("Sleeping")
|
134
|
+
end
|
135
|
+
sleep(one_hour)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Create an authorized Service instance.
|
140
|
+
#
|
141
|
+
# Note: An authorization failure will cause an exits, as it is a dire
|
142
|
+
# condition.
|
143
|
+
#
|
144
|
+
# Returns a new Service instance which has been properly authorized.
|
145
|
+
def service
|
146
|
+
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)
|
152
|
+
end
|
153
|
+
log.info("Logged in as #{@config[:username]}")
|
154
|
+
service
|
155
|
+
end
|
156
|
+
|
157
|
+
# Connect and authenticate to the PG&E Website.
|
158
|
+
#
|
159
|
+
# It provides an instance of Service to the provided block
|
160
|
+
# for direct manipulation.
|
161
|
+
#
|
162
|
+
# Returns nothing.
|
163
|
+
def connect
|
164
|
+
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
|
170
|
+
end
|
171
|
+
|
172
|
+
# Attempts to retrieve power data for each of the dates in the list.
|
173
|
+
#
|
174
|
+
# dates - An array of Date objects to retrieve power data for.
|
175
|
+
#
|
176
|
+
# Returns an Array of successfully retrieved dates.
|
177
|
+
def fetch_dates(dates)
|
178
|
+
completed = []
|
179
|
+
|
180
|
+
connect do |service|
|
181
|
+
dates.each do |date|
|
182
|
+
log.info("Fetching #{date}")
|
183
|
+
data = service.fetch_csv(date)
|
184
|
+
|
185
|
+
log.info("Verifying #{date}")
|
186
|
+
samples = Sample.parse_csv(data).values.first
|
187
|
+
first_sample = samples.first
|
188
|
+
|
189
|
+
if first_sample.kwh
|
190
|
+
log.info("Saving #{date}")
|
191
|
+
File.open(data_file(date), "w") do |f|
|
192
|
+
f.write(data)
|
193
|
+
end
|
194
|
+
|
195
|
+
upload(date, samples)
|
196
|
+
|
197
|
+
log.info("Completed #{date}")
|
198
|
+
completed << date
|
199
|
+
else
|
200
|
+
log.info("Incomplete #{date}")
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
completed
|
206
|
+
end
|
207
|
+
|
208
|
+
def upload(date, samples)
|
209
|
+
case @config[:transport]
|
210
|
+
when :google_powermeter
|
211
|
+
log.info("Uploading #{date} to Google PowerMeter")
|
212
|
+
transport = SmarterMeter::Transports::GooglePowerMeter.new(@config[:google_powermeter])
|
213
|
+
if transport.upload(samples)
|
214
|
+
log.info("Upload for #{date} complete")
|
215
|
+
else
|
216
|
+
log.info("Upload for #{date} failed")
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
# Returns an Array of Date objects containing all dates since start_date
|
222
|
+
# missing power data.
|
223
|
+
def dates_requiring_data
|
224
|
+
collected = Dir.glob(File.join(@config[:data_dir], "*-*-*.csv")).map { |f| File.basename(f, ".csv") }
|
225
|
+
all_days = []
|
226
|
+
|
227
|
+
count_of_days = (Date.today - @config[:start_date]).to_i
|
228
|
+
|
229
|
+
count_of_days.times do |i|
|
230
|
+
all_days << (@config[:start_date] + i).strftime("%Y-%m-%d")
|
231
|
+
end
|
232
|
+
|
233
|
+
(all_days - collected).map { |d| Date.parse(d) }
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module SmarterMeter
|
4
|
+
class Sample
|
5
|
+
attr_accessor :time, :kwh
|
6
|
+
|
7
|
+
# Parses the CSV returned by PG&E
|
8
|
+
#
|
9
|
+
# data - The string containing the CSV returned by PG&E
|
10
|
+
#
|
11
|
+
# Returns a Hash of with keys as Date objects and values of Arrays of samples.
|
12
|
+
def self.parse_csv(data)
|
13
|
+
samples = {}
|
14
|
+
date_re = /([0-9]{1,2})\/([0-9]{1,2})\/([0-9]{4})/
|
15
|
+
|
16
|
+
# Apparently they felt the need to put a = outside of the correct place
|
17
|
+
data = data.gsub('=','')
|
18
|
+
|
19
|
+
hour_increment = 1/24.0
|
20
|
+
CSV.parse(data) do |row|
|
21
|
+
next unless row.length > 0 and date_re.match row[0]
|
22
|
+
|
23
|
+
month, day, year = date_re.match(row[0]).captures
|
24
|
+
month = month.to_i
|
25
|
+
day = day.to_i
|
26
|
+
year = year.to_i
|
27
|
+
|
28
|
+
timestamp = Time.local(year, month, day, 0) - hour_increment + 1/(24.0*60)
|
29
|
+
hourly_samples = row[1..24].map do |v|
|
30
|
+
if v == "-"
|
31
|
+
kwh = nil
|
32
|
+
else
|
33
|
+
kwh = v.to_f
|
34
|
+
end
|
35
|
+
|
36
|
+
timestamp = timestamp + hour_increment
|
37
|
+
Sample.new(timestamp, kwh)
|
38
|
+
end
|
39
|
+
samples[Date.new(year, month, day)] = hourly_samples
|
40
|
+
end
|
41
|
+
samples
|
42
|
+
end
|
43
|
+
|
44
|
+
def initialize(time, kwh)
|
45
|
+
@time = time
|
46
|
+
@kwh = kwh
|
47
|
+
end
|
48
|
+
|
49
|
+
# Public: The start time of this measurement
|
50
|
+
#
|
51
|
+
# Returns the time in the format of 2011-01-06T09:00:00.000Z
|
52
|
+
def utc_start_time
|
53
|
+
@time.utc.strftime("%Y-%m-%dT%H:%M:%S.000Z")
|
54
|
+
end
|
55
|
+
|
56
|
+
# Public: The stop time of this measurement, it's assumed to be 1 hour after
|
57
|
+
# the start.
|
58
|
+
#
|
59
|
+
# Returns the time in the format of 2011-01-06T09:00:00.000Z
|
60
|
+
def utc_stop_time
|
61
|
+
(@time + 60*60).utc.strftime("%Y-%m-%dT%H:%M:%S.000Z")
|
62
|
+
end
|
63
|
+
|
64
|
+
def inspect
|
65
|
+
"<Sample #{@time} #{@kwh}>"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|