···11+Copyright (c) 2012 joshua stein <jcs@jcs.org>
22+33+Redistribution and use in source and binary forms, with or without
44+modification, are permitted provided that the following conditions
55+are met:
66+77+1. Redistributions of source code must retain the above copyright
88+ notice, this list of conditions and the following disclaimer.
99+2. Redistributions in binary form must reproduce the above copyright
1010+ notice, this list of conditions and the following disclaimer in the
1111+ documentation and/or other materials provided with the distribution.
1212+3. The name of the author may not be used to endorse or promote products
1313+ derived from this software without specific prior written permission.
1414+1515+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
1616+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
1717+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
1818+IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
1919+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
2020+NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
2121+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
2222+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
2323+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
2424+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+36
lib/config_hash.rb
···11+class ConfigHash
22+ CONFIG_FILE = "#{ENV["HOME"]}/.pullup_counter"
33+44+ def initialize
55+ @config = {}
66+ read
77+ end
88+99+ def [](var)
1010+ @config[var]
1111+ end
1212+1313+ def []=(var, val)
1414+ @config[var] = val
1515+ end
1616+1717+ def read
1818+ @config = {}
1919+2020+ if File.exists?(CONFIG_FILE)
2121+ File.read(CONFIG_FILE).split("\n").each do |line|
2222+ if m = line.strip.match(/^([^=]+)=(.*)/)
2323+ @config[m[1]] = m[2]
2424+ end
2525+ end
2626+ end
2727+ end
2828+2929+ def save!
3030+ File.open(CONFIG_FILE, "w+", 0600) do |f|
3131+ @config.each do |k,v|
3232+ f.puts "#{k}=#{v}"
3333+ end
3434+ end
3535+ end
3636+end
+36
lib/logger/beeper.rb
···11+class Beeper < Loggerish
22+ attr_accessor :player
33+44+ def self.args
55+ [
66+ [ "--sound", "-s", GetoptLong::REQUIRED_ARGUMENT,
77+ "play <sound> file" ],
88+ ]
99+ end
1010+1111+ def after_initialize
1212+ if @enabled = !!@parent.config["sound"]
1313+ if @parent.config["verbose"]
1414+ puts "#{Time.now.to_f} - playing sound file " <<
1515+ @parent.config["sound"]
1616+ end
1717+1818+ if `uname -s`.strip == "OpenBSD"
1919+ @player = [ "aucat", "-i" ]
2020+ else
2121+ # dunno, assume mac
2222+ @player = [ "afplay" ]
2323+ end
2424+ end
2525+ end
2626+2727+ def log_pullup!(time)
2828+ cmd = @player + [ @parent.config["sound"] ]
2929+3030+ if @parent.config["debug"]
3131+ puts "#{Time.now.to_f} - running " << cmd.inspect
3232+ end
3333+3434+ system(*cmd)
3535+ end
3636+end
+125
lib/logger/fitbit.rb
···11+require "oauth"
22+require "json"
33+44+class Fitbit < Loggerish
55+ def self.args
66+ [
77+ [ "--fitbit", "-F", GetoptLong::NO_ARGUMENT,
88+ "log to a Fitbit account (will walk through setup)" ],
99+ ]
1010+ end
1111+1212+ def after_initialize
1313+ if @enabled = !!@parent.config["fitbit"]
1414+ if @parent.config["verbose"]
1515+ puts "#{Time.now.to_f} - enabling Fitbit logging module"
1616+ end
1717+1818+ verify_keys
1919+ end
2020+ end
2121+2222+ def log_pullup!(time)
2323+ # TODO: update this to log to the user's custom tracker instead of an
2424+ # activity, because activities don't allow custom units and logging pullups
2525+ # in miles doesn't make any sense
2626+ json = self.oauth_request("/1/user/-/activities.json", :post, {
2727+ "activityName" => "Pullup",
2828+ "manualCalories" => 1,
2929+ "startTime" => time.strftime("%H:%M"),
3030+ "durationMillis" => 1000,
3131+ "date" => time.strftime("%Y-%m-%d"),
3232+ "distance" => "1.0",
3333+ })
3434+3535+ begin
3636+ h = JSON.parse(json)
3737+ if (id = h["activityLog"]["activityId"].to_i) != 0
3838+ if @parent.config["verbose"]
3939+ puts "#{Time.now.to_f} - logged pullup on fitbit (id #{id})"
4040+ end
4141+ else
4242+ raise "no activity id"
4343+ end
4444+4545+ rescue => e
4646+ puts "#{Time.now.to_f} - error from fitbit (#{e.message}): " <<
4747+ json.inspect
4848+ end
4949+ end
5050+5151+ def oauth_consumer
5252+ OAuth::Consumer.new(@parent.config["fitbit_oauth_key"],
5353+ @parent.config["fitbit_oauth_secret"],
5454+ { :site => "http://api.fitbit.com", :http_method => :get })
5555+ end
5656+5757+ def oauth_request(req, method = :get, post_data = nil)
5858+ begin
5959+ Timeout.timeout(20) do
6060+ at = OAuth::AccessToken.new(oauth_consumer,
6161+ @parent.config["fitbit_token"], @parent.config["fitbit_secret"])
6262+6363+ if method == :get
6464+ res = at.get(req, { "Accept-Language" => "en_US" })
6565+ elsif method == :post
6666+ res = at.post(req, post_data, { "Accept-Language" => "en_US" })
6767+ else
6868+ raise "what kind of method is #{method}?"
6969+ end
7070+7171+ if res.class.superclass != Net::HTTPSuccess
7272+ raise res.class.to_s
7373+ end
7474+7575+ return res.body
7676+ end
7777+ rescue Timeout::Error => e
7878+ puts "#{Time.now.to_f} - timed out talking to Fitbit: #{e.message}"
7979+ rescue StandardError => e
8080+ puts "#{Time.now.to_f} - error talking to Fitbit: #{e.message}"
8181+ end
8282+ end
8383+8484+ def verify_keys
8585+ while @parent.config["fitbit_oauth_key"].to_s == ""
8686+ puts "No Fitbit OAuth key found. Register an application at",
8787+ "https://dev.fitbit.com/apps/new with PIN authentication and enter ",
8888+ "the consumer key and secret here.",
8989+ ""
9090+9191+ print "OAuth consumer key: "
9292+ @parent.config["fitbit_oauth_key"] = STDIN.gets.strip
9393+ print "OAuth consumer secret: "
9494+ @parent.config["fitbit_oauth_secret"] = STDIN.gets.strip
9595+9696+ # this will be invalid now anyway
9797+ @parent.config["fitbit_token"] = ""
9898+9999+ puts ""
100100+ end
101101+102102+ while @parent.config["fitbit_token"].to_s == ""
103103+ request_token = oauth_consumer.get_request_token
104104+105105+ puts "No Fitbit token found. Authorize your Fitbit account at ",
106106+ request_token.authorize_url,
107107+ ""
108108+109109+ print "Enter the PIN received: "
110110+ pin = STDIN.gets.strip
111111+112112+ access_token = request_token.get_access_token(:oauth_verifier => pin)
113113+ if !access_token
114114+ raise "couldn't get access token from pin"
115115+ end
116116+117117+ @parent.config["fitbit_token"] = access_token.token
118118+ @parent.config["fitbit_secret"] = access_token.secret
119119+120120+ @parent.config.save!
121121+122122+ puts ""
123123+ end
124124+ end
125125+end
+27
lib/logger/textfile.rb
···11+class Textfile < Loggerish
22+ def self.args
33+ [
44+ [ "--file", "-f", GetoptLong::REQUIRED_ARGUMENT,
55+ "log to flat file <file>" ],
66+ ]
77+ end
88+99+ def after_initialize
1010+ if @enabled = !!@parent.config["file"]
1111+ if @parent.config["verbose"]
1212+ puts "#{Time.now.to_f} - enabling plaintext logging module to " <<
1313+ @parent.config["file"]
1414+ end
1515+ end
1616+ end
1717+1818+ def log_pullup!(time)
1919+ File.open(@parent.config["file"], "a") do |f|
2020+ f.puts time.strftime("%Y-%m-%d %H:%M:%S")
2121+ end
2222+2323+ if @parent.config["verbose"]
2424+ puts "#{Time.now.to_f} - logged to file #{@parent.config["file"]}"
2525+ end
2626+ end
2727+end