smartermeter 0.1.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/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
|