├── .rspec ├── Gemfile ├── lib ├── fitgem │ ├── version.rb │ ├── badges.rb │ ├── food_form.rb │ ├── errors.rb │ ├── devices.rb │ ├── locales.rb │ ├── sleep.rb │ ├── water.rb │ ├── blood_pressure.rb │ ├── glucose.rb │ ├── units.rb │ ├── heart_rate.rb │ ├── users.rb │ ├── alarms.rb │ ├── time_range.rb │ ├── friends.rb │ ├── notifications.rb │ ├── foods.rb │ ├── client.rb │ ├── helpers.rb │ ├── body_measurements.rb │ └── activities.rb └── fitgem.rb ├── .travis.yml ├── .yardopts ├── .gitignore ├── spec ├── spec_helper.rb ├── fitgem_constructor_spec.rb ├── fitgem_client_spec.rb ├── fitgem_spec.rb ├── fitgem_body_measurements_spec.rb ├── fitgem_activities_spec.rb ├── fitgem_notifications_spec.rb └── fitgem_helper_spec.rb ├── Rakefile ├── Guardfile ├── fitgem.gemspec ├── LICENSE └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /lib/fitgem/version.rb: -------------------------------------------------------------------------------- 1 | module Fitgem 2 | VERSION = '1.0.0' 3 | end 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 2.1 3 | - 2.2 4 | - 2.3.0 5 | before_install: 6 | - gem update bundler 7 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --no-private 2 | --protected 3 | --exclude /spec 4 | --exclude /pkg 5 | - 6 | changelog.md 7 | LICENSE 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | .yardoc/* 6 | test*.rb 7 | *.dmp 8 | .fitgem.yml 9 | .idea 10 | .rvmrc 11 | .ruby-version 12 | .ruby-gemset 13 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 3 | 4 | require 'fitgem' 5 | 6 | RSpec.configure do |config| 7 | config.mock_with :rspec 8 | end -------------------------------------------------------------------------------- /lib/fitgem.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'oauth2' 3 | require 'fitgem/client' 4 | 5 | # Read/write data with the {http://www.fitbit.com Fitbit.com} OAuth API. 6 | # 7 | # @author Zachery Moneypenny 8 | module Fitgem 9 | end 10 | -------------------------------------------------------------------------------- /lib/fitgem/badges.rb: -------------------------------------------------------------------------------- 1 | module Fitgem 2 | class Client 3 | 4 | # Get a list of all badges earned 5 | # 6 | # @return [Hash] Hash containing an array of badges 7 | def badges 8 | get("/user/#{@user_id}/badges.json") 9 | end 10 | 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | require 'rspec/core/rake_task' 3 | 4 | Bundler::GemHelper.install_tasks 5 | 6 | desc 'Default: run specs.' 7 | task :default => :spec 8 | 9 | desc "Run specs" 10 | RSpec::Core::RakeTask.new do |t| 11 | t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default. 12 | # Put spec opts in a file named .rspec in root 13 | end 14 | -------------------------------------------------------------------------------- /lib/fitgem/food_form.rb: -------------------------------------------------------------------------------- 1 | module Fitgem 2 | # Enumeration of food form types 3 | # 4 | # Primarily used in calls to {#create_food}. 5 | class FoodFormType 6 | # Food in liquid form 7 | # @return [String] 8 | def self.LIQUID 9 | "LIQUID" 10 | end 11 | 12 | # Food in dry form 13 | # @return [String] 14 | def self.DRY 15 | "DRY" 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/fitgem_constructor_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Fitgem::Client do 4 | describe "constructor" do 5 | it "requires consumer_key" do 6 | expect { 7 | opts = { :consumer_secret => "12345" } 8 | client = Fitgem::Client.new(opts) 9 | }.to raise_error(Fitgem::InvalidArgumentError, "Missing required options: consumer_key") 10 | end 11 | 12 | it "requires consumer_secret" do 13 | expect { 14 | opts = { :consumer_key => "12345" } 15 | client = Fitgem::Client.new(opts) 16 | }.to raise_error(Fitgem::InvalidArgumentError, "Missing required options: consumer_secret") 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/fitgem/errors.rb: -------------------------------------------------------------------------------- 1 | module Fitgem 2 | class InvalidArgumentError < ArgumentError 3 | end 4 | 5 | class UserIdError < Exception 6 | end 7 | 8 | class InvalidDateArgument < InvalidArgumentError 9 | end 10 | 11 | class InvalidTimeArgument < InvalidArgumentError 12 | end 13 | 14 | class InvalidTimeRange < InvalidArgumentError 15 | end 16 | 17 | class InvalidUnitSystem < InvalidArgumentError 18 | end 19 | 20 | class InvalidMeasurementType < InvalidArgumentError 21 | end 22 | 23 | class ConnectionRequiredError < Exception 24 | end 25 | 26 | class DeprecatedApiError < Exception 27 | end 28 | 29 | class ServiceUnavailableError < Exception 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | guard 'rspec', :cli => "--color --format Fuubar --fail-fast", :version => 2 do 5 | watch(%r{^spec/.+_spec\.rb$}) 6 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } 7 | watch('spec/spec_helper.rb') { "spec" } 8 | 9 | # Rails example 10 | watch(%r{^spec/.+_spec\.rb$}) 11 | watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 12 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } 13 | watch(%r{^spec/support/(.+)\.rb$}) { "spec" } 14 | watch('spec/spec_helper.rb') { "spec" } 15 | end 16 | -------------------------------------------------------------------------------- /lib/fitgem/devices.rb: -------------------------------------------------------------------------------- 1 | module Fitgem 2 | class Client 3 | # ========================================== 4 | # Device Retrieval Methods 5 | # ========================================== 6 | 7 | # Get a list of devices for the current account 8 | # 9 | # @return [Array] An array of hashes, each of which describes 10 | # a device attaached to the current account 11 | def devices 12 | get("/user/#{@user_id}/devices.json") 13 | end 14 | 15 | # Get the details about a specific device 16 | # 17 | # The ID required for this call could be found by getting the list 18 | # of devices with a call to {#devices}. 19 | # 20 | # @param [Integer, String] device_id The ID of the device to get 21 | # details for 22 | # @return [Hash] Hash containing device information 23 | def device_info(device_id) 24 | get("/user/#{@user_id}/devices/#{device_id}.json") 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /fitgem.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path('../lib', __FILE__) 3 | $:.push File.join(File.dirname(__FILE__), '.', 'lib') 4 | 5 | require 'fitgem/version' 6 | 7 | Gem::Specification.new do |s| 8 | s.name = 'fitgem' 9 | s.version = Fitgem::VERSION 10 | s.platform = Gem::Platform::RUBY 11 | s.authors = ['Zachery Moneypenny'] 12 | s.email = ['fitgem@whazzmaster.com'] 13 | s.homepage = 'http://github.com/whazzmaster/fitgem' 14 | s.summary = %q{OAuth client library to the data on fitbit.com} 15 | s.description = %q{A client library to send and retrieve workout, weight, and diet data from fitbit.com} 16 | 17 | s.rubyforge_project = 'fitgem' 18 | 19 | s.add_dependency 'oauth2' 20 | s.add_development_dependency 'rake' 21 | s.add_development_dependency 'rspec', '~> 3.0.0' 22 | s.add_development_dependency 'yard' 23 | s.add_development_dependency 'rdiscount' 24 | 25 | s.files = %w(LICENSE README.md fitgem.gemspec) + `git ls-files -z`.split("\x0").select { |f| f.start_with?("lib/") } 26 | s.require_paths = ['lib'] 27 | end 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Zachery Moneypenny 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/fitgem/locales.rb: -------------------------------------------------------------------------------- 1 | module Fitgem 2 | # Enumeration of valid locales that can be sent to fitbit 3 | # 4 | # Set the {Fitgem::Client#api_locale} property to one of 5 | # these values to set the locale used for all subsequent 6 | # API calls. 7 | # 8 | # See {https://wiki.fitbit.com/display/API/API+Localization} for 9 | # more information on how the various locales are used. 10 | class ApiLocale 11 | # US Locale 12 | # 13 | # Used by default in fitgem 14 | # 15 | # @return [String] 16 | def self.US 17 | "en_US" 18 | end 19 | 20 | # Australia Locale 21 | # 22 | # @return [String] 23 | def self.AU 24 | "en_AU" 25 | end 26 | 27 | # France Locale 28 | # 29 | # @return [String] 30 | def self.FR 31 | "fr_FR" 32 | end 33 | 34 | # Germany Locale 35 | # 36 | # @return [String] 37 | def self.DE 38 | "de_DE" 39 | end 40 | 41 | # Japan Locale 42 | # 43 | # @return [String] 44 | def self.JP 45 | "ja_JP" 46 | end 47 | 48 | # New Zealand Locale 49 | # 50 | # @return [String] 51 | def self.NZ 52 | "en_NZ" 53 | end 54 | 55 | # Spain Locale 56 | # 57 | # @return [String] 58 | def self.ES 59 | "es_ES" 60 | end 61 | 62 | # UK Locale 63 | # 64 | # @return [String] 65 | def self.UK 66 | "en_GB" 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/fitgem_client_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Fitgem::Client do 4 | let(:access_token) { 5 | double 'Access Token', :get => response, :request => response 6 | } 7 | let(:client) { Fitgem::Client.new({ 8 | :consumer_key => '12345', 9 | :consumer_secret => '67890' 10 | }) } 11 | let(:response) { double :body => {:foo => :bar}.to_json, :status => 200 } 12 | let(:consumer) { double 'Consumer' } 13 | 14 | before :each do 15 | allow(OAuth2::Client).to receive(:new).with('12345', '67890', { 16 | :site => "https://api.fitbit.com", 17 | :token_url => "https://api.fitbit.com/oauth2/token", 18 | :authorize_url => "https://www.fitbit.com/oauth2/authorize" 19 | }).and_return(consumer) 20 | allow(OAuth2::AccessToken).to receive(:new).and_return(access_token) 21 | end 22 | 23 | it 'returns JSON from the request' do 24 | expect(client.user_info).to eq({'foo' => 'bar'}) 25 | end 26 | 27 | it 'raises a service unavailable exception when the status is 503' do 28 | allow(response).to receive(:status).and_return(503) 29 | 30 | expect { client.user_info }.to raise_error(Fitgem::ServiceUnavailableError) 31 | end 32 | 33 | context "response with an blank body" do 34 | let(:response) { double :body => "", :status => 200 } 35 | 36 | it "is properly parsed" do 37 | blank_user_info = client.user_info 38 | expect(blank_user_info).to eq({}) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/fitgem/sleep.rb: -------------------------------------------------------------------------------- 1 | module Fitgem 2 | class Client 3 | # ========================================== 4 | # Sleep Retrieval Methods 5 | # ========================================== 6 | 7 | # Get sleep data for specified date 8 | # 9 | # @param [DateTime, Date, String] date 10 | # @return [Array] List of sleep items for the supplied date 11 | def sleep_on_date(date) 12 | get("/user/#{@user_id}/sleep/date/#{format_date(date)}.json") 13 | end 14 | 15 | # ========================================== 16 | # Sleep Logging Methods 17 | # ========================================== 18 | 19 | # Log sleep data to fitbit for current user 20 | # 21 | # All aspects of the options hash are REQUIRED. 22 | # 23 | # @param [Hash] opts Sleep data to log 24 | # @option opts [String] :startTime Hours and minutes in the format "HH:mm" 25 | # @option opts [Integer, String] :duration Sleep duration, in miliseconds 26 | # @option opts [DateTime, Date, String] :date Sleep log entry date; 27 | # if a string it must be in the yyyy-MM-dd format, or the values 28 | # 'today' or 'tomorrow' 29 | # 30 | # @return [Hash] Summary of the logged sleep data 31 | # 32 | # @since v0.4.0 33 | def log_sleep(opts) 34 | post("/user/#{@user_id}/sleep.json", opts) 35 | end 36 | 37 | # Delete logged sleep data 38 | # 39 | # The sleep log id is the one returned when sleep data is recorded 40 | # via {#log_sleep}. 41 | # 42 | # @param [Integer, String] Sleep log id 43 | # @return [Hash] Empty hash denotes successful deletion 44 | # 45 | # @since v0.4.0 46 | def delete_sleep_log(sleep_log_id) 47 | delete("/user/#{@user_id}/sleep/#{sleep_log_id}.json") 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/fitgem/water.rb: -------------------------------------------------------------------------------- 1 | module Fitgem 2 | class Client 3 | # ========================================== 4 | # Water Retrieval Methods 5 | # ========================================== 6 | 7 | # Get water log entries for the supplied date 8 | # 9 | # @param [DateTime, Date, String] date 10 | # @return [Hash] Hash containing the summary of the days logs, and a 11 | # list of all individual entries 12 | def water_on_date(date) 13 | get("/user/#{@user_id}/foods/log/water/date/#{format_date(date)}.json") 14 | end 15 | 16 | 17 | # ========================================== 18 | # Water Logging Methods 19 | # ========================================== 20 | 21 | # Log water consumption to fitbit 22 | # 23 | # @param [Hash] opts Water consumption data 24 | # @option opts [Integer, Decimal, String] :amount Amount of water 25 | # consumed; if String must be in "X.X" format 26 | # @option opts [String] :unit Unit of water consumed; valid values 27 | # are ("ml", "fl oz" or "cup") 28 | # @option opts [DateTime, Date, String] :date Date of water 29 | # consumption 30 | # 31 | # @return [Hash] Summary of logged information 32 | def log_water(opts) 33 | unless opts[:amount] && opts[:date] 34 | raise Fitgem::InvalidArgumentError, "Must include both an :amount and :date to log water" 35 | end 36 | 37 | opts[:date] = format_date(opts[:date]) 38 | post("/user/#{@user_id}/foods/log/water.json",opts) 39 | end 40 | 41 | # Delete logged water consumption 42 | # 43 | # @param [Integer, String] water_log_id The id of previously logged 44 | # water consumption 45 | # @return [Hash] Empty hash denotes success 46 | def delete_water_log(water_log_id) 47 | delete("/user/-/foods/log/water/#{water_log_id}.json") 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/fitgem/blood_pressure.rb: -------------------------------------------------------------------------------- 1 | module Fitgem 2 | class Client 3 | # ========================================== 4 | # Blood Pressure Retrieval Methods 5 | # ========================================== 6 | 7 | # Get blood pressure log entries for the supplied date 8 | # 9 | # @param [DateTime, Date, String] date 10 | # @return [Hash] Hash containing an average of the days logs, and a 11 | # list of all individual entries 12 | def blood_pressure_on_date(date) 13 | get("/user/#{@user_id}/bp/date/#{format_date(date)}.json") 14 | end 15 | 16 | # ========================================== 17 | # Blood Pressure Logging Methods 18 | # ========================================== 19 | 20 | # Log blood pressure information to fitbit 21 | # 22 | # @param [Hash] opts Heart rate data 23 | # @option opts [Integer, String] :systolic Systolic measurement (REQUIRED) 24 | # @option opts [Integer, String] :diastolic Diastolic measurement (REQUIRED) 25 | # @option opts [DateTime, Date, String] :date Log entry date (REQUIRED) 26 | # @option opts [DateTime, Time, String] :time Time of the measurement; hours and minutes in the format HH:mm 27 | # 28 | # @return [Hash] Summary of logged information 29 | def log_blood_pressure(opts) 30 | unless opts[:systolic] && opts[:diastolic] && opts[:date] 31 | raise Fitgem::InvalidArgumentError, "Must include :systolic, :diastolic, and :date in order to log blood pressure data" 32 | end 33 | 34 | opts[:date] = format_date(opts[:date]) 35 | opts[:time] = format_time(opts[:time]) if opts[:time] 36 | post("/user/#{@user_id}/bp.json", opts) 37 | end 38 | 39 | # Delete logged blood pressure information 40 | # 41 | # @param [Integer, String] blood_pressure_log_id The id of previously logged 42 | # blood pressure data 43 | # @return [Hash] Empty hash denotes success 44 | def delete_blood_pressure_log(blood_pressure_log_id) 45 | delete("/user/-/bp/#{blood_pressure_log_id}.json") 46 | end 47 | end 48 | end -------------------------------------------------------------------------------- /lib/fitgem/glucose.rb: -------------------------------------------------------------------------------- 1 | module Fitgem 2 | class Client 3 | # ========================================== 4 | # Glucose Retrieval Methods 5 | # ========================================== 6 | 7 | # Get glucose log entries for the supplied date 8 | # 9 | # @param [DateTime, Date, String] date 10 | # @return [Hash] Hash containing an average of the days logs, and a 11 | # list of all individual entries 12 | def glucose_on_date(date) 13 | get("/user/#{@user_id}/glucose/date/#{format_date(date)}.json") 14 | end 15 | 16 | # ========================================== 17 | # Glucose Logging Methods 18 | # ========================================== 19 | 20 | # Log glucose information to fitbit 21 | # 22 | # @param [Hash] opts Glucose data 23 | # @option opts [String] :tracker Glucose tracker name; 24 | # predefined or custom tracker name (matches tracker name on the website) (this or :hba1c is REQUIRED) 25 | # @option opts [String] :hba1c HbA1c measurement; in the format "X.X" (this or :tracker is REQUIRED) 26 | # @option opts [String] :glucose Glucose measurement; in the format "X.X" (REQUIRED with :tracker, OPTIONAL otherwise) 27 | # @option opts [DateTime, Date, String] :date Log entry date (REQUIRED) 28 | # @option opts [DateTime, String] :time Time of the measurement; hours and minutes in the format HH:mm 29 | # 30 | # @return [Hash] Summary of logged information 31 | def log_glucose(opts) 32 | unless opts[:tracker] || opts[:hba1c] 33 | raise Fitgem::InvalidArgumentError, "Must include :tracker or :hba1c in order to log glucose data" 34 | end 35 | 36 | if opts[:tracker] && opts[:hba1c].nil? && opts[:glucose].nil? 37 | raise Fitgem::InvalidArgumentError, "Must include :glucose if using :tracker with no :hba1c value in order to log glucose data" 38 | end 39 | 40 | opts[:date] = format_date(opts[:date]) 41 | opts[:time] = format_time(opts[:time]) if opts[:time] 42 | post("/user/#{@user_id}/glucose.json", opts) 43 | end 44 | end 45 | end -------------------------------------------------------------------------------- /lib/fitgem/units.rb: -------------------------------------------------------------------------------- 1 | module Fitgem 2 | # Enumeration of valid unit systems that can be sent to fitbit 3 | # 4 | # Set the {Fitgem::Client#api_unit_system} property to one of 5 | # these values to set the unit system used for all subsequent 6 | # API calls. 7 | # 8 | # See {https://wiki.fitbit.com/display/API/API-Unit-System} for 9 | # more information on how the various unit systems are used. 10 | class ApiUnitSystem 11 | # US Units 12 | # 13 | # Used by default in fitgem 14 | # 15 | # @return [String] 16 | def self.US 17 | "en_US" 18 | end 19 | 20 | # UK Units 21 | # 22 | # @return [String] 23 | def self.UK 24 | "en_GB" 25 | end 26 | 27 | # Metric Units 28 | # 29 | # @return [String] 30 | def self.METRIC 31 | "" 32 | end 33 | end 34 | 35 | # Enumeration of available distance units that may be used when 36 | # logging measurements or activities that involve distance. 37 | # 38 | # See {https://wiki.fitbit.com/display/API/API-Distance-Unit} for 39 | # more information about using distance units. 40 | class ApiDistanceUnit 41 | # @return [String] 42 | def self.centimeters 43 | "Centimeter" 44 | end 45 | 46 | # @return [String] 47 | def self.feet 48 | "Foot" 49 | end 50 | 51 | # @return [String] 52 | def self.inches 53 | "Inch" 54 | end 55 | 56 | # @return [String] 57 | def self.kilometers 58 | "Kilometer" 59 | end 60 | 61 | # @return [String] 62 | def self.meters 63 | "Meter" 64 | end 65 | 66 | # @return [String] 67 | def self.miles 68 | "Mile" 69 | end 70 | 71 | # @return [String] 72 | def self.millimeters 73 | "Millimeter" 74 | end 75 | 76 | # Steps are only valid distance units when logging walking or 77 | # running activities. The distance is computed from the stride 78 | # length the user provides. 79 | # 80 | # @return [String] 81 | def self.steps 82 | "Steps" 83 | end 84 | 85 | # @return [String] 86 | def self.yards 87 | "Yards" 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/fitgem/heart_rate.rb: -------------------------------------------------------------------------------- 1 | module Fitgem 2 | class Client 3 | # ========================================== 4 | # Heart Rate Retrieval Methods 5 | # ========================================== 6 | 7 | # Get heart rate log entries for the supplied date 8 | # 9 | # @param [DateTime, Date, String] date 10 | # @return [Hash] Hash containing an average of the days logs, and a 11 | # list of all individual entries 12 | def heart_rate_on_date(date) 13 | get("/user/#{@user_id}/heart/date/#{format_date(date)}.json") 14 | end 15 | 16 | # ========================================== 17 | # Heart Rate Logging Methods 18 | # ========================================== 19 | 20 | # Log heart rate information to fitbit 21 | # 22 | # @param [Hash] opts Heart rate data 23 | # @option opts [String] :tracker Heart rate tracker name; 24 | # predefined or custom tracker name (matches tracker name on the website) (REQUIRED) 25 | # @option opts [Integer, String] :heart_rate Heart rate measurement (REQUIRED) 26 | # @option opts [DateTime, Date, String] :date Log entry date (REQUIRED) 27 | # @option opts [DateTime, String] :time Time of the measurement; hours and minutes in the format HH:mm 28 | # 29 | # @return [Hash] Summary of logged information 30 | def log_heart_rate(opts) 31 | unless opts[:tracker] && opts[:heart_rate] && opts[:date] 32 | raise Fitgem::InvalidArgumentError, "Must include :tracker, :heart_rate, and :date in order to lof heart rate data" 33 | end 34 | 35 | opts[:heartRate] = opts.delete :heart_rate 36 | opts[:date] = format_date(opts[:date]) 37 | opts[:time] = format_time(opts[:time]) if opts[:time] 38 | post("/user/#{@user_id}/heart.json", opts) 39 | end 40 | 41 | # Delete logged heart rate information 42 | # 43 | # @param [Integer, String] heart_rate_log_id The id of previously logged 44 | # heart rate data 45 | # @return [Hash] Empty hash denotes success 46 | def delete_heart_rate_log(heart_rate_log_id) 47 | delete("/user/-/heart/#{heart_rate_log_id}.json") 48 | end 49 | end 50 | end -------------------------------------------------------------------------------- /spec/fitgem_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Fitgem do 4 | before do 5 | @client = Fitgem::Client.new({ 6 | :consumer_key => '12345', 7 | :consumer_secret => '67890' 8 | }) 9 | end 10 | 11 | describe 'global settings' do 12 | it 'should expose the api_version' do 13 | expect(@client.api_version).to eq "1" 14 | end 15 | 16 | it 'should all clients to set a new api version' do 17 | @client.api_version = "2" 18 | expect(@client.api_version).to eq "2" 19 | end 20 | 21 | it 'should default to the US unit system' do 22 | expect(@client.api_unit_system).to eq Fitgem::ApiUnitSystem.US 23 | end 24 | 25 | it 'should allow the unit system to be set to other types' do 26 | @client.api_unit_system = Fitgem::ApiUnitSystem.UK 27 | expect(@client.api_unit_system).to eq Fitgem::ApiUnitSystem.UK 28 | @client.api_unit_system = Fitgem::ApiUnitSystem.METRIC 29 | expect(@client.api_unit_system).to eq Fitgem::ApiUnitSystem.METRIC 30 | end 31 | 32 | it 'should default to a user id of \'-\', the currently-logged in user' do 33 | expect(@client.user_id).to eq '-' 34 | end 35 | 36 | it 'should default to the US locale' do 37 | expect(@client.api_locale).to eq Fitgem::ApiLocale.US 38 | end 39 | 40 | it 'should allow the locale to be set to other types' do 41 | @client.api_locale = Fitgem::ApiLocale.AU 42 | expect(@client.api_locale).to eq Fitgem::ApiLocale.AU 43 | @client.api_locale = Fitgem::ApiLocale.FR 44 | expect(@client.api_locale).to eq Fitgem::ApiLocale.FR 45 | @client.api_locale = Fitgem::ApiLocale.DE 46 | expect(@client.api_locale).to eq Fitgem::ApiLocale.DE 47 | @client.api_locale = Fitgem::ApiLocale.JP 48 | expect(@client.api_locale).to eq Fitgem::ApiLocale.JP 49 | @client.api_locale = Fitgem::ApiLocale.NZ 50 | expect(@client.api_locale).to eq Fitgem::ApiLocale.NZ 51 | @client.api_locale = Fitgem::ApiLocale.ES 52 | expect(@client.api_locale).to eq Fitgem::ApiLocale.ES 53 | @client.api_locale = Fitgem::ApiLocale.UK 54 | expect(@client.api_locale).to eq Fitgem::ApiLocale.UK 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/fitgem_body_measurements_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Fitgem::Client do 4 | before(:each) do 5 | @client = Fitgem::Client.new({ 6 | :consumer_key => '12345', 7 | :consumer_secret => '67890' 8 | }) 9 | end 10 | 11 | describe "#body_weight" do 12 | it "formats the correct URI based on a date" do 13 | expect(@client).to receive(:get).with("/user/-/body/log/weight/date/2013-04-26.json") 14 | @client.body_weight({:date => "2013-04-26"}) 15 | end 16 | 17 | it "formats the correct URI based on a base date and period" do 18 | expect(@client).to receive(:get).with("/user/-/body/log/weight/date/2013-09-27/30d.json") 19 | @client.body_weight({:base_date => "2013-09-27", :period => "30d"}) 20 | end 21 | 22 | it "formats the correct URI based on a base date and end date" do 23 | expect(@client).to receive(:get).with("/user/-/body/log/weight/date/2013-12-01/2013-12-28.json") 24 | @client.body_weight({:base_date => "2013-12-01", :end_date => "2013-12-28"}) 25 | end 26 | 27 | it "raises an error when none of the required options are specified" do 28 | expect { @client.body_weight({}) }.to raise_error(Fitgem::InvalidArgumentError) 29 | end 30 | end 31 | 32 | describe "#body_fat" do 33 | it "formats the correct URI based on a date" do 34 | expect(@client).to receive(:get).with("/user/-/body/log/fat/date/2013-04-26.json") 35 | @client.body_fat({:date => "2013-04-26"}) 36 | end 37 | 38 | it "formats the correct URI based on a base date and period" do 39 | expect(@client).to receive(:get).with("/user/-/body/log/fat/date/2013-09-27/30d.json") 40 | @client.body_fat({:base_date => "2013-09-27", :period => "30d"}) 41 | end 42 | 43 | it "formats the correct URI based on a base date and end date" do 44 | expect(@client).to receive(:get).with("/user/-/body/log/fat/date/2013-12-01/2013-12-28.json") 45 | @client.body_fat({:base_date => "2013-12-01", :end_date => "2013-12-28"}) 46 | end 47 | 48 | it "raises an error when none of the required options are specified" do 49 | expect { @client.body_fat({}) }.to raise_error(Fitgem::InvalidArgumentError) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/fitgem/users.rb: -------------------------------------------------------------------------------- 1 | module Fitgem 2 | class Client 3 | # Get information aobut current user 4 | # 5 | # @param [Hash] opts User information request data 6 | # @return [Hash] User information 7 | def user_info(opts={}) 8 | get("/user/#{@user_id}/profile.json", opts) 9 | end 10 | 11 | # Update profile information for current user 12 | # 13 | # @param [Hash] opts User profile information 14 | # @option opts [String] :gender Gender, valid values are MALE, FEMALE, NA 15 | # @option opts [DateTime, Date, String] :birthday Birthday, in 16 | # "yyyy-MM-dd" if a String 17 | # @option opts [Decimal, Integer, String] :height Height, in format 18 | # "X.XX" if a String 19 | # @option opts [String] :nickname Nickname 20 | # @option opts [String] :aboutMe About Me description 21 | # @option opts [String] :fullName Full name 22 | # @option opts [String] :country; two-character code 23 | # @option opts [String] :state; two-character code 24 | # @option opts [String] :city 25 | # @option opts [Decimal, Integer, String] :strideLengthWalking Walking 26 | # stride length, in format "X.XX" if a String 27 | # @option opts [Decimal, Integer, String] :strideLengthRunning Running 28 | # stride length, in format "X.XX" if a String 29 | # @option opts [String] :weightUnit Default water unit on website 30 | # (doesn't affect API); one of (en_US, en_GB, "any" for METRIC) 31 | # @option opts [String] :heightUnit Default height/distance unit 32 | # on website (doesn't affect API); one of (en_US, "any" for METRIC) 33 | # @option opts [String] :waterUnit Default water unit on website 34 | # (doesn't affect API); one of (en_US, "any" for METRIC) 35 | # @option opts [String] :glucoseUnit Default glucose unit on website 36 | # (doesn't affect API); one of (en_US, "any" for METRIC) 37 | # @option opts [String] :timezone Time zone; in the format 38 | # "America/Los Angelos" 39 | # @option opts [String] :foodsLocale Food Database Locale; in the 40 | # format "xx_XX" 41 | # @option opts [String] :locale Locale of website (country/language); 42 | # one of the locales, see https://wiki.fitbit.com/display/API/API-Update-User-Info 43 | # for the currently supported values. 44 | # 45 | # @return [Hash] Hash containing updated profile information 46 | def update_user_info(opts) 47 | opts[:birthday] = format_date(opts[:birthday]) if opts[:birthday] 48 | post("/user/#{@user_id}/profile.json", opts) 49 | end 50 | 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fitgem [![Build Status](https://secure.travis-ci.org/whazzmaster/fitgem.png)](http://travis-ci.org/whazzmaster/fitgem) 2 | 3 | Provides access to [fitbit.com](http://www.fitbit.com) data through [their REST 4 | API](http://dev.fitbit.com). Fitgem can pull data with or without a valid 5 | OAUth access_token. Without an access_token you can only gather data that a 6 | user has denoted as 'public'. However, if an access_token is provided then all 7 | exposed data can be gathered for the logged-in account. 8 | 9 | ### WARNING: THIS GEM IS NO LONGER UNDER ACTIVE DEVELOPMENT 10 | 11 | **I'm looking for a new maintainer- I've grown to dislike Fitbit's products and 12 | platform and don't have much interest in this library anymore. Please contact 13 | me if you're interested in taking over development.** 14 | 15 | **The final release will be 1.0.0, which changes over the gem to use OAuth2.** 16 | 17 | ### Installation 18 | 19 | Install fitgem 20 | 21 | ```bash 22 | $ gem install fitgem 23 | ``` 24 | 25 | or add it to your Gemfile 26 | 27 | ```ruby 28 | gem 'fitgem' 29 | ``` 30 | 31 | ### API Reference 32 | Comprehensive method documentation is [available 33 | online](http://www.rubydoc.info/gems/fitbit/0.2.0/frames). 34 | 35 | The best way to connect your users to the Fitbit API is to use 36 | [omniauth-fitbit](https://github.com/tkgospodinov/omniauth-fitbit) to integrate 37 | Fitbit accounts into your web application. Once you have a Fitbit API OAuth 38 | `access_token` for a user it's simple to create a client object through fitgem 39 | to send and receive fitness data. 40 | 41 | ### Contributing to Fitgem 42 | The Fitbit REST API is in BETA right now, and so it will quite likely change 43 | over time. I aim to keep as up-to-date as I can but if you absolutely need 44 | functionality that isn't included here, **feel free to fork and implement it, 45 | then send me a pull request**. 46 | 47 | * Check out the latest master to make sure the feature hasn't been implemented 48 | or the bug hasn't been fixed yet 49 | * Check out the issue tracker to make sure someone already hasn't requested it 50 | and/or contributed it 51 | * Fork the project 52 | * Start a feature/bugfix branch 53 | * Commit and push until you are happy with your contribution 54 | * **Make sure to add tests for it**. This is important so I don't break it in a 55 | future version unintentionally. 56 | * **Please try not to mess with the Rakefile, version, or history**. If you 57 | want to have your own version, or is otherwise necessary, that is fine, but 58 | please isolate to its own commit so I can cherry-pick around it. 59 | 60 | #### Contributors 61 | 62 | **Many, many thanks** to [everyone that has contributed to improve 63 | fitgem](https://github.com/whazzmaster/fitgem/graphs/contributors)! 64 | 65 | #### License 66 | 67 | See LICENSE for further details. 68 | -------------------------------------------------------------------------------- /lib/fitgem/alarms.rb: -------------------------------------------------------------------------------- 1 | module Fitgem 2 | class Client 3 | # Add a silent alarm to your profile 4 | # 5 | # @param [Hash] opts Alarm options 6 | # @option opts [Integer, String] device_id The id of the device you would like 7 | # to manage the alarm on 8 | # @option opts [String] :time Time of the alarm in the format XX:XX+XX:XX, time 9 | # with timezone 10 | # @option opts [TrueClass, FalseClass] :enabled 11 | # @option opts [TrueClass, FalseClass] :recurring One time or recurring alarm 12 | # @option opts [String] :weekDays The days the alarm is active on as a list of 13 | # comma separated values: MONDAY, WEDNESDAY, SATURDAY. For recurring only 14 | # @option opts [Integer] :snoozeLength Minutes between the alarms 15 | # @option opts [Integer] :snoozeCount Maximum snooze count 16 | # @option opts [String] :label Label for the alarm 17 | # 18 | # @return [Hash] Hash containing the alarm settings 19 | def add_alarm(opts) 20 | device_id = opts[:device_id] 21 | post("/user/#{@user_id}/devices/tracker/#{device_id}/alarms.json", opts) 22 | end 23 | 24 | # Retrieve the silent alarms of your device 25 | # 26 | # @param [Integer, String] device_id The id of the device you would like to 27 | # manage the alarms of 28 | # 29 | # @return [Hash] Hash containing the alarms and it's settings 30 | def get_alarms(device_id) 31 | get("/user/#{@user_id}/devices/tracker/#{device_id}/alarms.json") 32 | end 33 | 34 | # Remove a silent alarm from your profile 35 | # 36 | # @param [Integer, String] alarm_id The id of the alarm 37 | # @param [Integer, String] device_id The id of the device you would like to 38 | # manage the alarm on 39 | # 40 | # @return [Hash] Empty hash denotes success 41 | def delete_alarm(alarm_id, device_id) 42 | delete("/user/#{@user_id}/devices/tracker/#{device_id}/alarms/#{alarm_id}.json") 43 | end 44 | 45 | # Update an existing alarm 46 | # 47 | # @param [Integer, String] alarm_id The id of the alarm 48 | # @param [Integer, String] device_id The id of the device you would like to 49 | # manage the alarm on 50 | # @param [Hash] opts Alarm settings 51 | # @option opts [DateTime, Time, String] :time Time of the alarm 52 | # @option opts [TrueClass, FalseClass] :enabled 53 | # @option opts [TrueClass, FalseClass] :recurring One time or recurring alarm 54 | # @option opts [String] :weekDays The days the alarm is active on as a list of 55 | # comma separated values: MONDAY, WEDNESDAY, SATURDAY. For recurring only 56 | # @option opts [Integer] :snoozeLength Minutes between the alarms 57 | # @option opts [Integer] :snoozeCount Maximum snooze count 58 | # @option opts [String] :label Label for the alarm 59 | # 60 | # @return [Hash] Hash containing updated alarm settings 61 | def update_alarm(alarm_id, device_id, opts) 62 | opts[:time] = format_time opts[:time], include_timezone: true 63 | post("/user/#{@user_id}/devices/tracker/#{device_id}/alarms/#{alarm_id}.json", opts) 64 | end 65 | 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/fitgem/time_range.rb: -------------------------------------------------------------------------------- 1 | module Fitgem 2 | class Client 3 | # Gets historical resource data in the time range specified by 4 | # options hash param. The time range can either be specified by 5 | # :base_date and :end_date OR by using :base_date and a :period 6 | # (supported periods are 1d, 7d, 30d, 1w, 1m, 3m, 6m, 1y, max) 7 | # 8 | # Supported values for +resource_path+ are: 9 | # 10 | # Food: 11 | # /foods/log/caloriesIn 12 | # /foods/log/water 13 | # 14 | # Activity: 15 | # /activities/log/calories 16 | # /activities/log/steps 17 | # /activities/log/distance 18 | # /activities/log/floors 19 | # /activities/log/elevation 20 | # /activities/log/minutesSedentary 21 | # /activities/log/minutesLightlyActive 22 | # /activities/log/minutesFairlyActive 23 | # /activities/log/minutesVeryActive 24 | # /activities/log/activeScore 25 | # /activities/log/activityCalories 26 | # 27 | # Raw Tracker Activity: 28 | # /activities/tracker/calories 29 | # /activities/tracker/steps 30 | # /activities/tracker/distance 31 | # /activities/tracker/floors 32 | # /activities/tracker/elevation 33 | # /activities/tracker/activeScore 34 | # 35 | # Sleep: 36 | # /sleep/startTime 37 | # /sleep/timeInBed 38 | # /sleep/minutesAsleep 39 | # /sleep/awakeningsCount 40 | # /sleep/minutesAwake 41 | # /sleep/minutesToFallAsleep 42 | # /sleep/minutesAfterWakeup 43 | # /sleep/efficiency 44 | # 45 | # Body: 46 | # /body/weight 47 | # /body/bmi 48 | # /body/fat 49 | # 50 | # @param [String] resource_path The resource path to get data for 51 | # (see note above) 52 | # @param [Hash] opts The options to specify date ranges, etc. 53 | # @option opts [DateTime, Date, String] :base_date The start date of the period 54 | # @option opts [DateTime, Date, String] :end_date The end date of the period 55 | # @option opts [String] :period The period (valid values: 1d, 7d, 30d, 1w, 1m, 3m, 6m, 1y, max) 56 | # 57 | # @return [Hash] Hash containing an array of objects that match your 58 | # request 59 | # 60 | # @example To get the floors traveled for the week of October 24th 61 | # data_by_time_range("/activities/log/floors", {:base_date => "2011-10-24", :period => "7d"}) 62 | # 63 | # @example To get all of the body weight logs from April 3rd until July 27th 64 | # data_by_time_range("/body/weight", {:base_date => "2011-03-03", :end_date => "2011-07-27"}) 65 | # 66 | def data_by_time_range(resource_path, opts) 67 | range = construct_date_range_fragment(opts) 68 | get("/user/#{@user_id}#{resource_path}/#{range}.json") 69 | end 70 | 71 | # protected 72 | 73 | def construct_date_range_fragment(options) 74 | range_str = "date/" 75 | if options[:base_date] && options[:period] 76 | range_str += "#{format_date(options[:base_date])}/#{options[:period]}" 77 | elsif options[:base_date] && options[:end_date] 78 | range_str += "#{format_date(options[:base_date])}/#{format_date(options[:end_date])}" 79 | else 80 | raise Fitgem::InvalidTimeRange, "Must supply either base_date and period OR base_date and end_date" 81 | end 82 | range_str 83 | end 84 | 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/fitgem/friends.rb: -------------------------------------------------------------------------------- 1 | module Fitgem 2 | class Client 3 | # ========================================== 4 | # Friend Retrieval Methods 5 | # ========================================== 6 | 7 | # Get a list of the current user's friends 8 | # 9 | # @return [Array] List of friends, each of which is a Hash 10 | # containing friend information 11 | def friends 12 | get("/user/#{@user_id}/friends.json") 13 | end 14 | 15 | # Get the leaderboard of friends' weekly activities 16 | # 17 | # @return [Hash] Friends' information 18 | def leaderboard 19 | get("/user/#{@user_id}/friends/leaderboard.json") 20 | end 21 | 22 | # Get the leaderboard of friends' weekly activities 23 | # 24 | # @return [Hash] Friends' information 25 | # 26 | # @deprecated Monthly leaderboards are no longer available from Fitbit. Please update to use {#leaderboard} 27 | def monthly_leaderboard 28 | raise DeprecatedApiError, 'Fitbit no longer exposes a monthly leaderboard. See https://wiki.fitbit.com/display/API/API-Get-Friends-Leaderboard for more information. Use #leaderboard() instead.' 29 | end 30 | 31 | # Get the leaderboard of friends' weekly activities 32 | # 33 | # @return [Hash] Friends' information 34 | # 35 | # @deprecated Please update to use {#leaderboard} 36 | def weekly_leaderboard 37 | raise DeprecatedApiError, 'Fitbit now only exposes a weekly leaderboard, Use #leaderboard() instead.' 38 | end 39 | 40 | # ========================================== 41 | # Invitation Management Methods 42 | # ========================================== 43 | 44 | # Create an invite for a user to connect with the current user as a friend 45 | # 46 | # In order to invite a user, either an :email or a valid :userId 47 | # must be supplied in the +opts+ param hash. 48 | # 49 | # @param [Hash] opts The invite data 50 | # @option opts [String] :email Email address of user to invite 51 | # @option opts [String] :userId User ID of the user to invite 52 | # 53 | # @return [Hash] 54 | def invite_friend(opts) 55 | unless opts[:email] || opts[:user_id] 56 | raise InvalidArgumentError.new "invite_friend hash argument must include :email or :user_id" 57 | end 58 | translated_options = {} 59 | translated_options[:invitedUserEmail] = opts[:email] if opts[:email] 60 | translated_options[:invitedUserId] = opts[:user_id] if opts[:user_id] 61 | post("/user/#{@user_id}/friends/invitations.json", translated_options) 62 | end 63 | 64 | # Accept a friend invite 65 | # 66 | # @param [String] requestor_id The ID of the requestor that sent the 67 | # friend request 68 | # @return [Hash] 69 | def accept_invite(requestor_id) 70 | respond_to_invite(requestor_id, true) 71 | end 72 | 73 | # Decline a friend invite 74 | # 75 | # @param [String] requestor_id The ID of the requestor that sent the 76 | # friend request 77 | # @return [Hash] 78 | def decline_invite(requestor_id) 79 | respond_to_invite(requestor_id, false) 80 | end 81 | 82 | private 83 | 84 | def respond_to_invite(requestor_id, does_accept) 85 | options = { :accept => does_accept.to_s } 86 | post("/user/#{@user_id}/friends/invitations/#{requestor_id}.json", options) 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/fitgem/notifications.rb: -------------------------------------------------------------------------------- 1 | module Fitgem 2 | class Client 3 | SUBSCRIBABLE_TYPES = [:sleep, :body, :activities, :foods, :all] 4 | 5 | # Get a list of all subscriptions 6 | # 7 | # @param [Hash] opts Subscription query data 8 | # @option opts [Integer, String] :type The type of subscription (valid 9 | # values are :activities, :foods, :sleep, :body, and :all). REQUIRED 10 | # 11 | # @return [Hash] Hash contain subscription information 12 | # @since v0.4.0 13 | def subscriptions(opts) 14 | get make_subscription_url(opts), make_headers(opts) 15 | end 16 | 17 | # Creates a notification subscription 18 | # 19 | # @note You must check the HTTP response code to check the status of the request to add a subscription. See {https://wiki.fitbit.com/display/API/Fitbit+Subscriptions+API} for information about what the codes mean. 20 | # 21 | # @param [Hash] opts The notification subscription data 22 | # @option opts [Symbol] :type The type of subscription (valid 23 | # values are :activities, :foods, :sleep, :body, and :all). REQUIRED 24 | # @option opts [Integer, String] :subscriptionId The subscription id 25 | # 26 | # @return [Integer, Hash] An array containing the HTTP response code and 27 | # a hash containing confirmation information for the subscription. 28 | # @since v0.4.0 29 | def create_subscription(opts) 30 | resp = raw_post make_subscription_url(opts.merge({:use_subscription_id => true})), EMPTY_BODY, make_headers(opts) 31 | [resp.status, extract_response_body(resp)] 32 | end 33 | 34 | # Removes a notification subscription 35 | # 36 | # @note You must check the HTTP response code to check the status of the request to remove a subscription. See {https://wiki.fitbit.com/display/API/Fitbit+Subscriptions+API} for information about what the codes mean. 37 | # 38 | # @param [Hash] opts The notification subscription data 39 | # @option opts [Symbol] :type The type of subscription to remove; 40 | # valid values are :activities, :foods, :sleep, :body, and :all). 41 | # REQUIRED 42 | # @option opts [Integer, String] :subscription_id The id of the 43 | # subscription to remove. 44 | # @option opts [Inteter, Stri)g] :subscriber_id The subscriber id of the client 45 | # application, created via {http://dev.fitbit.com} 46 | # 47 | # @return [Integer, Hash] An array containing the HTTP response code and 48 | # a hash containing confirmation information for the subscription. 49 | # @since v0.4.0 50 | def remove_subscription(opts) 51 | resp = raw_delete make_subscription_url(opts.merge({:use_subscription_id => true})), make_headers(opts) 52 | [resp.status, extract_response_body(resp)] 53 | end 54 | 55 | protected 56 | 57 | # Ensures that the type supplied is valid 58 | # 59 | # @param [Symbol] subscription_type The type of subscription; 60 | # valid values are (:sleep, :body, :activities, :foods, and 61 | # :all) 62 | # @raise [Fitgem::InvalidArgumentError] Raised if the supplied type 63 | # is not valid 64 | # @return [Boolean] 65 | def validate_subscription_type(subscription_type) 66 | unless subscription_type && SUBSCRIBABLE_TYPES.include?(subscription_type) 67 | raise Fitgem::InvalidArgumentError, "Invalid subscription type (valid values are #{SUBSCRIBABLE_TYPES.join(', ')})" 68 | end 69 | true 70 | end 71 | 72 | # Create the headers hash for subscription management calls 73 | # 74 | # This method both adds approriate headers given what is in the 75 | # options hash, as well as removes extraneous hash entries that are 76 | # not needed in the headers of the request sent to the API. 77 | # 78 | # @param [Hash] opts The options for header creation 79 | # @option opts [String] :subscriber_id The subscriber id of the client 80 | # application, created via {http://dev.fitbit.com} 81 | # @return [Hash] The headers has to pass to the get/post/put/delete 82 | # methods 83 | def make_headers(opts) 84 | headers = {} 85 | if opts[:subscriber_id] 86 | headers['X-Fitbit-Subscriber-Id'] = opts[:subscriber_id] 87 | end 88 | headers 89 | end 90 | 91 | # Create the subscription management API url 92 | # 93 | # @param [Hash] opts The options on how to construct the 94 | # subscription API url 95 | # @option opts [Symbol] :type The type of subscription; 96 | # valid values are (:sleep, :body, :activities, :foods, and 97 | # :all) 98 | # @option opts [Symbol] :use_subscription_id If true, then 99 | # opts[:subscription_id] will be used in url construction 100 | # @option opts [String] :subscription_id The id of the subscription 101 | # that the URL is for 102 | # @return [String] The url to use for subscription management 103 | def make_subscription_url(opts) 104 | validate_subscription_type opts[:type] 105 | path = if opts[:type] == :all 106 | "" 107 | else 108 | "/"+opts[:type].to_s 109 | end 110 | url = "/user/#{@user_id}#{path}/apiSubscriptions" 111 | if opts[:use_subscription_id] 112 | unless opts[:subscription_id] 113 | raise Fitgem::InvalidArgumentError, "Must include options[:subscription_id]" 114 | end 115 | url += "/#{opts[:subscription_id]}" 116 | end 117 | url += ".json" 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /spec/fitgem_activities_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Fitgem::Client do 4 | before do 5 | @client = Fitgem::Client.new({ 6 | :consumer_key => '12345', 7 | :consumer_secret => '67890' 8 | }) 9 | end 10 | 11 | describe '#create_or_update_daily_goal' do 12 | before(:each) do 13 | @opts = {type: :steps, value: '10000'} 14 | end 15 | 16 | it 'raises an exception if the :type value is missing' do 17 | @opts.delete :type 18 | expect { 19 | @client.create_or_update_daily_goal @opts 20 | }.to raise_error(Fitgem::InvalidArgumentError) 21 | end 22 | 23 | it 'raises an exception if the :type value is not valid' do 24 | @opts[:type] = :milesWalked 25 | expect { 26 | @client.create_or_update_daily_goal @opts 27 | }.to raise_error(Fitgem::InvalidArgumentError) 28 | end 29 | 30 | it 'raises an exception if the :value value is missing' do 31 | @opts.delete :value 32 | expect { 33 | @client.create_or_update_daily_goal @opts 34 | }.to raise_error(Fitgem::InvalidArgumentError) 35 | end 36 | 37 | it 'posts to the correct URI if the :type and :value are valid' do 38 | expect(@client).to receive(:post).with('/user/-/activities/goals/daily.json', @opts) 39 | @client.create_or_update_daily_goal @opts 40 | end 41 | end 42 | 43 | describe '#create_or_update_weekly_goal' do 44 | before(:each) do 45 | @opts = { type: :steps, value: '10000' } 46 | end 47 | 48 | it 'raises an exception if the :type value is missing' do 49 | @opts.delete :type 50 | expect { 51 | @client.create_or_update_weekly_goal @opts 52 | }.to raise_error(Fitgem::InvalidArgumentError) 53 | end 54 | 55 | it 'raises an exception if the :type value is not valid' do 56 | @opts[:type] = :milesWalked 57 | expect { 58 | @client.create_or_update_weekly_goal @opts 59 | }.to raise_error(Fitgem::InvalidArgumentError) 60 | end 61 | 62 | it 'raises an exception if the :value value is missing' do 63 | @opts.delete :value 64 | expect { 65 | @client.create_or_update_weekly_goal @opts 66 | }.to raise_error(Fitgem::InvalidArgumentError) 67 | end 68 | 69 | it 'posts to the correct URI if the :type and :value are valid' do 70 | expect(@client).to receive(:post).with('/user/-/activities/goals/weekly.json', @opts) 71 | @client.create_or_update_weekly_goal @opts 72 | end 73 | end 74 | 75 | describe '#intraday_time_series' do 76 | before(:each) do 77 | @date_opts = { 78 | resource: :calories, 79 | date: '2013-05-13', 80 | detailLevel: '1min' 81 | } 82 | @time_opts = { 83 | resource: :calories, 84 | date: '2013-05-13', 85 | detailLevel: '15min', 86 | startTime: '10:00', 87 | endTime: '13:00' 88 | } 89 | end 90 | 91 | it 'raises an exception if the resource is missing' do 92 | expect { 93 | @client.intraday_time_series(@date_opts.merge!(resource: nil)) 94 | }.to raise_error(Fitgem::InvalidArgumentError) 95 | end 96 | 97 | it 'raises an exception if the resource is not valid' do 98 | expect { 99 | @client.intraday_time_series(@date_opts.merge!(resource: :some_wrong_thing)) 100 | }.to raise_error(Fitgem::InvalidArgumentError) 101 | end 102 | 103 | it 'valid resources should not raise an error' do 104 | allow(@client).to receive(:raw_get) 105 | [:calories, :steps, :distance, :floors, :elevation].each do |resource| 106 | opts = { resource: resource, date: '2013-05-13', detailLevel: '15min' } 107 | expect{ 108 | @client.intraday_time_series(opts) 109 | }.not_to raise_error 110 | end 111 | end 112 | 113 | it 'raises an exception if the date is missing' do 114 | expect { 115 | @client.intraday_time_series(@date_opts.merge!(date: nil)) 116 | }.to raise_error(Fitgem::InvalidArgumentError) 117 | end 118 | 119 | it 'raises an exception if the date is invalid' do 120 | expect { 121 | @client.intraday_time_series(@date_opts.merge!(date: 'zach-is-cool')) 122 | }.to raise_error(Fitgem::InvalidDateArgument) 123 | end 124 | 125 | it 'raises an exception if the detail level is missing' do 126 | expect { 127 | @client.intraday_time_series(@date_opts.merge!(detailLevel: nil)) 128 | }.to raise_error(Fitgem::InvalidArgumentError) 129 | end 130 | 131 | it 'raises an exception if the detail level is invalid' do 132 | expect { 133 | @client.intraday_time_series(@date_opts.merge!(detailLevel: '5years')) 134 | }.to raise_error(Fitgem::InvalidArgumentError) 135 | end 136 | 137 | it 'raises an exception if only the start time is supplied' do 138 | expect { 139 | @client.intraday_time_series(@time_opts.merge!(endTime: nil)) 140 | }.to raise_error(Fitgem::InvalidArgumentError) 141 | end 142 | 143 | it 'raises an exception if only the end time is supplied' do 144 | expect { 145 | @client.intraday_time_series(@time_opts.merge!(startTime: nil)) 146 | }.to raise_error(Fitgem::InvalidArgumentError) 147 | end 148 | 149 | it 'raises an exception if the start time is invalid' do 150 | expect { 151 | @client.intraday_time_series(@time_opts.merge!(startTime: 'what-is-this-nonsense')) 152 | }.to raise_error(Fitgem::InvalidTimeArgument) 153 | end 154 | 155 | it 'raises an exception if the end time is invalid' do 156 | expect { 157 | @client.intraday_time_series(@time_opts.merge!(endTime: 'what-is-this-nonsense')) 158 | }.to raise_error(Fitgem::InvalidTimeArgument) 159 | end 160 | 161 | it 'constructs the correct time-based url' do 162 | expect(@client).to receive(:get).with('/user/-/activities/calories/date/2013-05-13/1d/15min/time/10:00/13:00.json', {}) 163 | @client.intraday_time_series(@time_opts) 164 | end 165 | 166 | it 'constructs the correct date-based url' do 167 | expect(@client).to receive(:get).with('/user/-/activities/calories/date/2013-05-13/1d/1min.json', {}) 168 | @client.intraday_time_series(@date_opts) 169 | end 170 | end 171 | end 172 | -------------------------------------------------------------------------------- /lib/fitgem/foods.rb: -------------------------------------------------------------------------------- 1 | module Fitgem 2 | class Client 3 | # ========================================== 4 | # Food Retrieval Methods 5 | # ========================================== 6 | 7 | # Get all foods logged on the supplied date 8 | # 9 | # @param [DateTime, Date, String] date 10 | # @return [Array] A list of the foods, each of which is a 11 | # Hash containing the food details 12 | def foods_on_date(date) 13 | get("/user/#{@user_id}/foods/log/date/#{format_date(date)}.json") 14 | end 15 | 16 | # Get a list of the recently logged foods 17 | # 18 | # @return [Array] A list of foods, each of which is a Hash 19 | # containing the food details 20 | def recent_foods 21 | get("/user/#{@user_id}/foods/log/recent.json") 22 | end 23 | 24 | # Get a list of the frequently logged foods 25 | # 26 | # @return [Array] A list of foods, each of which is a Hash 27 | # containing the food details 28 | def frequent_foods 29 | get("/user/#{@user_id}/foods/log/frequent.json") 30 | end 31 | 32 | # Get a list of the favorite logged foods 33 | # 34 | # @return [Array] A list of foods, each of which is a Hash 35 | # containing the food details 36 | def favorite_foods 37 | get("/user/#{@user_id}/foods/log/favorite.json") 38 | end 39 | 40 | # Get a list of all of the available food units 41 | # 42 | # @return [Array] List of food units 43 | def foods_units 44 | get('/foods/units.json') 45 | end 46 | 47 | # Get a hash containing the user's meal informarion 48 | # 49 | # @return [Hash] Meal information 50 | # 51 | # @since v0.9.0 52 | def meals 53 | get("/user/#{@user_id}/meals.json") 54 | end 55 | 56 | # ========================================== 57 | # Food Query Methods 58 | # ========================================== 59 | 60 | # Search the foods database 61 | # 62 | # @param [String] query The query parameters for the food search 63 | # @return [Array] A list of foods, each of which is a Hash 64 | # containing detailed food information 65 | def find_food(query) 66 | get("/foods/search.json?query=#{URI.escape(query)}") 67 | end 68 | 69 | # Get detailed information for a food 70 | # 71 | # @param [Integer, String] food_id 72 | # @return [Hash] Hash containing detailed food information 73 | def food_info(food_id) 74 | get("/foods/#{food_id}.json") 75 | end 76 | 77 | # ========================================== 78 | # Food Update Methods 79 | # ========================================== 80 | 81 | # Log food to fitbit for the current user 82 | # 83 | # To log a food, either a +foodId+ or +foodName+ is REQUIRED. 84 | # 85 | # @param [Hash] opts Food log options 86 | # @option opts [Integer, String] :foodId Food id 87 | # @option opts [String] :foodName Food entry name 88 | # @option opts [String] :brandName Brand name, valid only with foodName 89 | # @option opts [Integer, String] :calories Calories for this serving size, 90 | # valid only with foodName 91 | # @option opts [Integer, String] :mealTypeId Meal type id; (1 - 92 | # Breakfast, 2 - Morning Snack, 3 - Lunch, 4 - Afternoon Snack, 5 - Dinner, 7 - Anytime) 93 | # @option opts [Integer, String] :unitId Unit id; typically 94 | # retrieved via previous calls to {#foods_on_date}, 95 | # {#recent_foods}, {#favorite_foods}, {#frequent_foods}, 96 | # {#find_food}, or {#foods_units} 97 | # @option opts [Integer, Decimal, String] :amount Amount consumed; 98 | # if a String, must be in the format "X.XX" 99 | # @option opts [DateTime, Date, String] :date Log entry date; in the 100 | # format "yyyy-MM-dd" if a String 101 | # @option opts [Boolean] :favorite Add food to favorites after 102 | # creating the log 103 | # 104 | # @return [Hash] Hash containing confirmation of the logged food 105 | def log_food(opts) 106 | post("/user/#{@user_id}/foods/log.json", opts) 107 | end 108 | 109 | # Mark a food as a favorite 110 | # 111 | # @param [Integer, String] food_id Food id 112 | # @return [Hash] Empty denotes success 113 | def add_favorite_food(food_id) 114 | post("/user/#{@user_id}/foods/log/favorite/#{food_id}.json") 115 | end 116 | 117 | # ========================================== 118 | # Food Removal Methods 119 | # ========================================== 120 | 121 | # Remove a logged food entry 122 | # 123 | # @param [Integer, String] food_log_id The ID of the food log, which 124 | # is the ID returned in the response when {#log_food} is called. 125 | # @return [Hash] Empty hash denotes success 126 | def delete_logged_food(food_log_id) 127 | delete("/user/#{@user_id}/foods/log/#{food_log_id}.json") 128 | end 129 | 130 | # Unmark a food as a favorite 131 | # 132 | # @param [Integer, String] food_id Food id 133 | # @return [Hash] Empty hash denotes success 134 | def remove_favorite_food(food_id) 135 | delete("/user/#{@user_id}/foods/favorite/#{food_id}.json") 136 | end 137 | 138 | # ========================================== 139 | # Food Creation Methods 140 | # ========================================== 141 | 142 | # Create a new food and add it to fitbit 143 | # 144 | # @param [Hash] opts The data used to create the food 145 | # @option opts [String] :name Food name 146 | # @option opts [Integer, String] :defaultFoodMeasurementUnitId Unit id; typically 147 | # retrieved via previous calls to {#foods_on_date}, 148 | # {#recent_foods}, {#favorite_foods}, {#frequent_foods}, 149 | # {#find_food}, or {#foods_units} 150 | # @option opts [Integer, String] :defaultServingSize The default 151 | # serving size of the food 152 | # @option opts [Integer, String] :calories The number of calories in 153 | # the default serving size 154 | # @option opts [String] :formType Form type; LIQUID or DRY - use 155 | # {Fitgem::FoodFormType#LIQUID} or {Fitgem::FoodFormType#DRY} 156 | # @option opts [String] :description Food description 157 | # 158 | # @return [Hash] If successful, returns a hash containing 159 | # confirmation of the food that was added 160 | def create_food(opts) 161 | post("/foods.json", opts) 162 | end 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /lib/fitgem/client.rb: -------------------------------------------------------------------------------- 1 | require 'fitgem/version' 2 | require 'fitgem/helpers' 3 | require 'fitgem/errors' 4 | require 'fitgem/users' 5 | require 'fitgem/activities' 6 | require 'fitgem/sleep' 7 | require 'fitgem/water' 8 | require 'fitgem/blood_pressure' 9 | require 'fitgem/glucose' 10 | require 'fitgem/heart_rate' 11 | require 'fitgem/units' 12 | require 'fitgem/foods' 13 | require 'fitgem/friends' 14 | require 'fitgem/body_measurements' 15 | require 'fitgem/time_range' 16 | require 'fitgem/devices' 17 | require 'fitgem/notifications' 18 | require 'fitgem/alarms' 19 | require 'fitgem/badges' 20 | require 'fitgem/locales' 21 | require 'date' 22 | require 'uri' 23 | 24 | module Fitgem 25 | class Client 26 | API_VERSION = '1' 27 | EMPTY_BODY = '' 28 | 29 | # Sets or gets the api_version to be used in API calls 30 | #" 31 | # @return [String] 32 | attr_accessor :api_version 33 | 34 | # Sets or gets the api unit system to be used in API calls 35 | # 36 | # @return [String] 37 | # 38 | # @example Set this using the {Fitgem::ApiUnitSystem} 39 | # client.api_unit_system = Fitgem::ApiUnitSystem.UK 40 | # @example May also be set in the constructor call 41 | # client = Fitgem::Client { 42 | # :consumer_key => my_key, 43 | # :consumer_secret => my_secret, 44 | # :token => fitbit_oauth_token, 45 | # :unit_system => Fitgem::ApiUnitSystem.METRIC 46 | # } 47 | attr_accessor :api_unit_system 48 | 49 | # Sets or gets the api locale to be used in API calls 50 | # 51 | # @return [String] 52 | # 53 | # @example Set this using the {Fitgem::ApiLocale} 54 | # client.api_locale = Fitgem::ApiLocale.UK 55 | # @example May also be set in the constructor call 56 | # client = Fitgem::Client { 57 | # :consumer_key => my_key, 58 | # :consumer_secret => my_secret, 59 | # :token => fitbit_oauth_token, 60 | # :unit_system => Fitgem::ApiUnitSystem.METRIC, 61 | # :locale => Fitgem::ApiLocale.JP 62 | # } 63 | attr_accessor :api_locale 64 | 65 | # Sets or gets the user id to be used in API calls 66 | # 67 | # @return [String] 68 | attr_accessor :user_id 69 | 70 | # Creates a client object to communicate with the fitbit API 71 | # 72 | # There are two primary ways to create a client: one if the current 73 | # fitbit user has not authenticated through fitbit.com, and another 74 | # if they have already authenticated and you have a stored 75 | # token returned by fitbit after the user authenticated and 76 | # authorized your application. 77 | # 78 | # @param [Hash] opts The constructor options 79 | # @option opts [String] :consumer_key The consumer key (required for 80 | # OAuth) 81 | # @option opts [String] :consumer_secret The consumer secret (required 82 | # for OAuth) 83 | # @option opts [String] :token The token generated by fitbit during the OAuth 84 | # handshake; stored and re-passed to the constructor to create a 85 | # 'logged-in' client 86 | # @option opts [String] :user_id The Fitbit user id of the logged-in 87 | # user 88 | # @option opts [Symbol] :unit_system The unit system to use for API 89 | # calls; use {Fitgem::ApiUnitSystem} to set during initialization. 90 | # DEFAULT: {Fitgem::ApiUnitSystem.US} 91 | # @option opts [Symbol] :locale The locale to use for API calls; 92 | # use {Fitgem::ApiLocale} to set during initialization. 93 | # DEFAULT: {Fitgem::ApiLocale.US} 94 | # 95 | # @example User has not yet authorized with fitbit 96 | # client = Fitgem::Client.new { :consumer_key => my_key, :consumer_secret => my_secret } 97 | # 98 | # @example User has already authorized with fitbit, and we have a stored token 99 | # client = Fitgem::Client.new { 100 | # :consumer_key => my_key, 101 | # :token => fitbit_oauth_token, 102 | # } 103 | # 104 | # @return [Client] A Fitgem::Client; may be in a logged-in state or 105 | # ready-to-login state 106 | def initialize(opts) 107 | missing = [:consumer_key, :consumer_secret] - opts.keys 108 | if missing.size > 0 109 | raise Fitgem::InvalidArgumentError, "Missing required options: #{missing.join(',')}" 110 | end 111 | @consumer_key = opts[:consumer_key] 112 | @consumer_secret = opts[:consumer_secret] 113 | 114 | @token = opts[:token] 115 | @user_id = opts[:user_id] || '-' 116 | 117 | @api_unit_system = opts[:unit_system] || Fitgem::ApiUnitSystem.US 118 | @api_version = API_VERSION 119 | @api_locale = opts[:locale] || Fitgem::ApiLocale.US 120 | end 121 | 122 | private 123 | 124 | def consumer 125 | @consumer ||= OAuth2::Client.new(@consumer_key, @consumer_secret, { 126 | :site => 'https://api.fitbit.com', 127 | :token_url => 'https://api.fitbit.com/oauth2/token', 128 | :authorize_url => 'https://www.fitbit.com/oauth2/authorize' 129 | }) 130 | end 131 | 132 | def access_token 133 | @access_token ||= OAuth2::AccessToken.new(consumer, @token) 134 | end 135 | 136 | def get(path, headers={}) 137 | extract_response_body raw_get(path, headers) 138 | end 139 | 140 | def raw_get(path, headers={}) 141 | request(:get, path, headers: headers) 142 | end 143 | 144 | def post(path, body='', headers={}) 145 | extract_response_body raw_post(path, body, headers) 146 | end 147 | 148 | def raw_post(path, body='', headers={}) 149 | request(:post, path, body: body, headers: headers) 150 | end 151 | 152 | def delete(path, headers={}) 153 | extract_response_body raw_delete(path, headers) 154 | end 155 | 156 | def raw_delete(path, headers={}) 157 | request(:delete, path, headers: headers) 158 | end 159 | 160 | def request(verb, path, opts) 161 | versioned_path = "/#{@api_version}#{path}" 162 | opts.fetch(:headers) { {} }.merge! default_headers 163 | 164 | access_token.request(verb, versioned_path, opts) 165 | end 166 | 167 | def extract_response_body(response) 168 | return {} if response.nil? 169 | 170 | raise ServiceUnavailableError if response.status == 503 171 | 172 | return {} if response.body.nil? 173 | return {} if response.body.empty? 174 | 175 | JSON.parse(response.body) 176 | end 177 | 178 | def default_headers 179 | { 180 | 'User-Agent' => "fitgem gem v#{Fitgem::VERSION}", 181 | 'Accept-Language' => @api_unit_system, 182 | 'Accept-Locale' => @api_locale 183 | } 184 | end 185 | end 186 | end 187 | -------------------------------------------------------------------------------- /spec/fitgem_notifications_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'ostruct' 3 | 4 | describe Fitgem::Client do 5 | before(:each) do 6 | @client = Fitgem::Client.new({:consumer_key => '12345', :consumer_secret => '56789'}) 7 | end 8 | 9 | describe "#subscriptions" do 10 | before(:each) do 11 | allow(@client).to receive(:get) 12 | end 13 | 14 | it "calls #make_headers to create the headers for the API call" do 15 | opts = { :subscriber_id => "5555", :type => :all } 16 | expect(@client).to receive(:make_headers).with({:type=>:all, :subscriber_id=>"5555"}) 17 | @client.subscriptions(opts) 18 | end 19 | 20 | it "calls #get with the correct url and headers" do 21 | opts = { :subscriber_id => "5555", :type => :all } 22 | expect(@client).to receive(:get).with("/user/-/apiSubscriptions.json", {"X-Fitbit-Subscriber-Id"=>"5555"}) 23 | @client.subscriptions(opts) 24 | end 25 | end 26 | 27 | describe "#create_subscription" do 28 | before(:each) do 29 | @resp = OpenStruct.new 30 | allow(@client).to receive(:raw_post).and_return(@resp) 31 | end 32 | 33 | it "adds the :use_subscription_id flag and calls #make_headers" do 34 | opts = { :subscriber_id => "5555", :type => :all, :subscription_id => "320" } 35 | expect(@client).to receive(:make_headers).with({ :subscriber_id => "5555", :type => :all, :subscription_id => "320"}) 36 | @client.create_subscription(opts) 37 | end 38 | 39 | it "calls #raw_post with the correct url and headers for :all collection type" do 40 | opts = { :subscriber_id => "5555", :type => :all, :subscription_id => "320", :use_subscription_id => true } 41 | expect(@client).to receive(:raw_post).once.with("/user/-/apiSubscriptions/320.json", "", {"X-Fitbit-Subscriber-Id"=>"5555"}) 42 | @client.create_subscription(opts) 43 | end 44 | 45 | it "calls #raw_post with the correct url and headers for :sleep collection type" do 46 | opts = { :subscriber_id => "5555", :type => :sleep, :subscription_id => "320", :use_subscription_id => true } 47 | expect(@client).to receive(:raw_post).once.with("/user/-/sleep/apiSubscriptions/320.json", "", {"X-Fitbit-Subscriber-Id"=>"5555"}) 48 | @client.create_subscription(opts) 49 | end 50 | 51 | it "calls #extract_response_body to get the JSON body" do 52 | opts = { :subscriber_id => "5555", :type => :all, :subscription_id => "320", :use_subscription_id => true } 53 | expect(@client).to receive(:extract_response_body) 54 | @client.create_subscription(opts) 55 | end 56 | 57 | it "returns the code and the JSON body in an array" do 58 | opts = { :subscriber_id => "5555", :type => :all, :subscription_id => "320", :use_subscription_id => true } 59 | expect(@resp).to receive(:status).twice 60 | expect(@client.create_subscription(opts)).to be_a(Array) 61 | end 62 | end 63 | 64 | describe "#remove_subscription" do 65 | before(:each) do 66 | @resp = OpenStruct.new 67 | allow(@client).to receive(:raw_delete).and_return(@resp) 68 | end 69 | 70 | it "adds the :use_subscription_id flag and calls #make_headers" do 71 | opts = { :subscriber_id => "5555", :type => :all, :subscription_id => "320" } 72 | expect(@client).to receive(:make_headers).with({ :subscriber_id => "5555", :type => :all, :subscription_id => "320" }) 73 | @client.remove_subscription(opts) 74 | end 75 | 76 | it "calls #raw_delete with the correct url and headers for :all collection type" do 77 | opts = { :subscriber_id => "5555", :type => :all, :subscription_id => "320", :use_subscription_id => true } 78 | expect(@client).to receive(:raw_delete).once.with("/user/-/apiSubscriptions/320.json", {"X-Fitbit-Subscriber-Id"=>"5555"}) 79 | @client.remove_subscription(opts) 80 | end 81 | 82 | it "calls #extract_response_body to get the JSON body" do 83 | opts = { :subscriber_id => "5555", :type => :all, :subscription_id => "320", :use_subscription_id => true } 84 | expect(@client).to receive(:extract_response_body) 85 | @client.remove_subscription(opts) 86 | end 87 | 88 | it "returns the code and the JSON body in an array" do 89 | opts = { :subscriber_id => "5555", :type => :all, :subscription_id => "320", :use_subscription_id => true } 90 | expect(@resp).to receive(:status).twice 91 | expect(@client.remove_subscription(opts)).to be_a(Array) 92 | end 93 | end 94 | 95 | describe "#validate_subscription_types" do 96 | it "raises an exception if an invalid type is passed in" do 97 | expect { 98 | @client.send(:validate_subscription_type, :every_single_thing) 99 | }.to raise_error Fitgem::InvalidArgumentError, "Invalid subscription type (valid values are sleep, body, activities, foods, all)" 100 | end 101 | 102 | it "raises an exception if no type is supplied" do 103 | opts = { :opt1 => 'hello!' } 104 | expect { 105 | @client.send(:validate_subscription_type, opts[:type]) 106 | }.to raise_error Fitgem::InvalidArgumentError 107 | end 108 | end 109 | 110 | describe "#make_headers" do 111 | it "adds the subscriber id header" do 112 | opts = { :subscriber_id => '5555', :subscription_id => '320-activity' } 113 | headers = @client.send(:make_headers, opts) 114 | expect(headers.size).to eq 1 115 | expect(headers['X-Fitbit-Subscriber-Id']).to eq "5555" 116 | end 117 | end 118 | 119 | describe "#make_subscription_url" do 120 | it "creates the correct URL when no specific subscription id is used" do 121 | opts = { :subscription_id => "320", :type => :all } 122 | expect(@client.send(:make_subscription_url, opts)).to eq "/user/-/apiSubscriptions.json" 123 | end 124 | 125 | it "creates the correct URL for :all collection types" do 126 | opts = { :subscription_id => "320", :type => :all, :use_subscription_id => true } 127 | expect(@client.send(:make_subscription_url, opts)).to eq "/user/-/apiSubscriptions/320.json" 128 | end 129 | 130 | it "creates the correct URL for the :sleep collection type" do 131 | opts = { :subscription_id => "320", :type => :sleep, :use_subscription_id => true } 132 | expect(@client.send(:make_subscription_url, opts)).to eq "/user/-/sleep/apiSubscriptions/320.json" 133 | end 134 | 135 | it "creates the correct URL for the :body collection type" do 136 | opts = { :subscription_id => "320", :type => :body, :use_subscription_id => true } 137 | expect(@client.send(:make_subscription_url, opts)).to eq "/user/-/body/apiSubscriptions/320.json" 138 | end 139 | 140 | it "creates the correct URL for the :activities collection type" do 141 | opts = { :subscription_id => "320", :type => :activities, :use_subscription_id => true } 142 | expect(@client.send(:make_subscription_url, opts)).to eq "/user/-/activities/apiSubscriptions/320.json" 143 | end 144 | 145 | it "creates the correct URL for the :foods collection type" do 146 | opts = { :subscription_id => "320", :type => :foods, :use_subscription_id => true } 147 | expect(@client.send(:make_subscription_url, opts)).to eq "/user/-/foods/apiSubscriptions/320.json" 148 | end 149 | 150 | it "validates the supplied subscription type" do 151 | opts = { :subscription_id => "320" } 152 | expect { @client.send(:make_subscription_url, opts) }.to raise_error Fitgem::InvalidArgumentError 153 | 154 | opts[:type] = nil 155 | expect { @client.send(:make_subscription_url, opts) }.to raise_error Fitgem::InvalidArgumentError 156 | 157 | opts[:type] = :all 158 | expect { @client.send(:make_subscription_url, opts) }.not_to raise_error 159 | 160 | opts[:type] = :activities 161 | expect { @client.send(:make_subscription_url, opts) }.not_to raise_error 162 | 163 | opts[:type] = :sleep 164 | expect { @client.send(:make_subscription_url, opts) }.not_to raise_error 165 | 166 | opts[:type] = :foods 167 | expect { @client.send(:make_subscription_url, opts) }.not_to raise_error 168 | 169 | opts[:type] = :body 170 | expect { @client.send(:make_subscription_url, opts) }.not_to raise_error 171 | end 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /lib/fitgem/helpers.rb: -------------------------------------------------------------------------------- 1 | module Fitgem 2 | class Client 3 | # Format any of a variety of date types into the formatted string 4 | # required when using the fitbit API. 5 | # 6 | # The date parameter can take several different kind of objects: a 7 | # DateTime object, a Date object, a Time object or a String object. Furthermore, 8 | # the string object may be either the date in a preformatted string 9 | # ("yyyy-MM-dd"), or it may be the string values "today" or 10 | # "tomorrow". 11 | # 12 | # @param [DateTime, Date, Time, String] date The object to format into a 13 | # date string 14 | # @raise [Fitgem::InvalidDateArgument] Raised when the object is 15 | # not a DateTime, Date, Time or a valid (yyyy-MM-dd) String object 16 | # @return [String] Date in "yyyy-MM-dd" string format 17 | def format_date(date) 18 | if date.is_a? String 19 | case date 20 | when 'today' 21 | return Date.today.strftime("%Y-%m-%d") 22 | when 'yesterday' 23 | return (Date.today-1).strftime("%Y-%m-%d") 24 | else 25 | unless date =~ /\d{4}\-\d{2}\-\d{2}/ 26 | raise Fitgem::InvalidDateArgument, "Invalid date (#{date}), must be in yyyy-MM-dd format" 27 | end 28 | return date 29 | end 30 | elsif Date === date || Time === date || DateTime === date 31 | return date.strftime("%Y-%m-%d") 32 | else 33 | raise Fitgem::InvalidDateArgument, "Date used must be a date/time object or a string in the format YYYY-MM-DD; supplied argument is a #{date.class}" 34 | end 35 | end 36 | 37 | # Format any of a variety of time-related types into the formatted string 38 | # required when using the fitbit API. 39 | # 40 | # The time parameter can take several different kind of objects: a DateTime object, 41 | # a Time object, or a String Object. Furthermore, the string object may be either 42 | # the date in a preformatted string ("HH:mm"), or it may be the string value "now" to 43 | # indicate that the time value used is the current localtime. 44 | # 45 | # @param [DateTime, Time, String] time The object to format into a time string 46 | # @param [Hash] opts format time options 47 | # @option opts [TrueClass, FalseClass] :include_timezone Include timezone in the output or not 48 | # @raise [Fitgem::InvalidTimeArgument] Raised when the parameter object is not a 49 | # DateTime, Time, or a valid ("HH:mm" or "now") string object 50 | # @return [String] Date in "HH:mm" string format 51 | def format_time(time, opts = {}) 52 | format = opts[:include_timezone] ? "%H:%M%:z" : "%H:%M" 53 | if time.is_a? String 54 | case time 55 | when 'now' 56 | return DateTime.now.strftime format 57 | else 58 | unless time =~ /^\d{2}\:\d{2}$/ 59 | raise Fitgem::InvalidTimeArgument, "Invalid time (#{time}), must be in HH:mm format" 60 | end 61 | timezone = DateTime.now.strftime("%:z") 62 | return opts[:include_timezone] ? [ time, timezone ].join : time 63 | end 64 | elsif DateTime === time || Time === time 65 | return time.strftime format 66 | else 67 | raise Fitgem::InvalidTimeArgument, "Date used must be a valid time object or a string in the format HH:mm; supplied argument is a #{time.class}" 68 | end 69 | end 70 | 71 | # Fetch the correct label for the desired measurement unit. 72 | # 73 | # The general use case for this method is that you are using the client for 74 | # a specific user, and wish to get the correct labels for the unit measurements 75 | # returned for that user. 76 | # 77 | # A secondary use case is that you wish to get the label for a measurement given a unit 78 | # system that you supply (by setting the Fitgem::Client.api_unit_system attribute). 79 | # 80 | # In order for this method to get the correct value for the current user's preferences, 81 | # the client must have the ability to make API calls. If you respect_user_unit_preferences 82 | # is passed as 'true' (or left as the default value) and the client cannot make API calls 83 | # then an error will be raised by the method. 84 | # 85 | # @param [Symbol] measurement_type The measurement type to fetch the label for 86 | # @param [Boolean] respect_user_unit_preferences Should the method fetch the current user's 87 | # specific measurement preferences and use those (true), or use the value set on Fitgem::Client.api_unit_system (false) 88 | # @raise [Fitgem::ConnectionRequiredError] Raised when respect_user_unit_preferences is true but the 89 | # client is not capable of making API calls. 90 | # @raise [Fitgem::InvalidUnitSystem] Raised when the current value of Fitgem::Client.api_unit_system 91 | # is not one of [ApiUnitSystem.US, ApiUnitSystem.UK, ApiUnitSystem.METRIC] 92 | # @raise [Fitgem::InvalidMeasurementType] Raised when the supplied measurement_type is not one of 93 | # [:duration, :distance, :elevation, :height, :weight, :measurements, :liquids, :blood_glucose] 94 | # @return [String] The string label corresponding to the measurement type and 95 | # current api_unit_system. 96 | def label_for_measurement(measurement_type, respect_user_unit_preferences=true) 97 | unless [:duration, :distance, :elevation, :height, :weight, :measurements, :liquids, :blood_glucose].include?(measurement_type) 98 | raise InvalidMeasurementType, "Supplied measurement_type parameter must be one of [:duration, :distance, :elevation, :height, :weight, :measurements, :liquids, :blood_glucose], current value is :#{measurement_type}" 99 | end 100 | 101 | selected_unit_system = api_unit_system 102 | 103 | if respect_user_unit_preferences 104 | unless connected? 105 | raise ConnectionRequiredError, "No connection to Fitbit API; one is required when passing respect_user_unit_preferences=true" 106 | end 107 | # Cache the unit systems for the current user 108 | @unit_systems ||= self.user_info['user'].select {|key, value| key =~ /Unit$/ } 109 | 110 | case measurement_type 111 | when :distance 112 | selected_unit_system = @unit_systems["distanceUnit"] 113 | when :height 114 | selected_unit_system = @unit_systems["heightUnit"] 115 | when :liquids 116 | selected_unit_system = @unit_systems["waterUnit"] 117 | when :weight 118 | selected_unit_system = @unit_systems["weightUnit"] 119 | when :blood_glucose 120 | selected_unit_system = @unit_systems["glucoseUnit"] 121 | else 122 | selected_unit_system = api_unit_system 123 | end 124 | end 125 | 126 | # Fix the METRIC system difference 127 | selected_unit_system = Fitgem::ApiUnitSystem.METRIC if selected_unit_system == "METRIC" 128 | 129 | # Ensure the target unit system is one that we know about 130 | unless [ApiUnitSystem.US, ApiUnitSystem.UK, ApiUnitSystem.METRIC].include?(selected_unit_system) 131 | raise InvalidUnitSystem, "The select unit system must be one of [ApiUnitSystem.US, ApiUnitSystem.UK, ApiUnitSystem.METRIC], current value is #{selected_unit_system}" 132 | end 133 | 134 | unit_mappings[selected_unit_system][measurement_type] 135 | end 136 | 137 | # Recursively turns arrays and hashes into symbol-key based 138 | # structures. 139 | # 140 | # @param [Array, Hash] The structure to symbolize keys for 141 | # @return A new structure with the keys symbolized 142 | def self.symbolize_keys(obj) 143 | case obj 144 | when Array 145 | obj.inject([]){|res, val| 146 | res << case val 147 | when Hash, Array 148 | symbolize_keys(val) 149 | else 150 | val 151 | end 152 | res 153 | } 154 | when Hash 155 | obj.inject({}){|res, (key, val)| 156 | nkey = case key 157 | when String 158 | key.to_sym 159 | else 160 | key 161 | end 162 | nval = case val 163 | when Hash, Array 164 | symbolize_keys(val) 165 | else 166 | val 167 | end 168 | res[nkey] = nval 169 | res 170 | } 171 | else 172 | obj 173 | end 174 | end 175 | 176 | protected 177 | 178 | # Defined mappings for unit measurements to labels 179 | def unit_mappings 180 | { 181 | ApiUnitSystem.US => { :duration => "milliseconds", :distance => "miles", :elevation => "feet", :height => "inches", :weight => "pounds", :measurements => "inches", :liquids => "fl oz", :blood_glucose => "mg/dL" }, 182 | ApiUnitSystem.UK => { :duration => "milliseconds", :distance => "kilometers", :elevation => "meters", :height => "centimeters", :weight => "stone", :measurements => "centimeters", :liquids => "mL", :blood_glucose => "mmol/l" }, 183 | ApiUnitSystem.METRIC => { :duration => "milliseconds", :distance => "kilometers", :elevation => "meters", :height => "centimeters", :weight => "kilograms", :measurements => "centimeters", :liquids => "mL", :blood_glucose => "mmol/l" } 184 | } 185 | end 186 | 187 | end 188 | end 189 | -------------------------------------------------------------------------------- /lib/fitgem/body_measurements.rb: -------------------------------------------------------------------------------- 1 | module Fitgem 2 | class Client 3 | # ========================================== 4 | # Body Measurements Methods 5 | # ========================================== 6 | 7 | # Get the body measurements logged on a specified date 8 | # 9 | # @param [DateTime, String] date The date to retrieve body 10 | # measurements for. May be a DateTime object or a String 11 | # in 'yyyy-MM-dd' format. 12 | # @return [Hash] A hash containing the logged body measurements 13 | # along with any goals set for the current user. 14 | def body_measurements_on_date(date) 15 | get("/user/#{@user_id}/body/date/#{format_date(date)}.json") 16 | end 17 | 18 | # Get a list of logged body weight entries for the specified period 19 | # 20 | # @params [Hash] opts body weight options 21 | # @option opts [Date] date The date in the format YYYY-mm-dd. 22 | # @option opts [Date] base_date The end date when period is provided, in the 23 | # format yyyy-MM-dd or today; range start date when a date range is provided. 24 | # @option opts [String] period The date range period. One of 1d, 7d, 30d, 1w, 1m 25 | # @option opts [Date] end_date Range end date when date range is provided. 26 | # Note that period should not be longer than 31 day 27 | # 28 | # @return [Hash] A hash containing body weight log entries 29 | # 30 | # @since v0.9.0 31 | def body_weight(opts = {}) 32 | get determine_body_uri("/user/#{@user_id}/body/log/weight", opts) 33 | end 34 | 35 | # Retrieve the body weight goal of the current user 36 | # 37 | # @return [Hash] A hash containing the start date, 38 | # start weight and current weight. 39 | # 40 | # @since v0.9.0 41 | def body_weight_goal 42 | get("/user/#{@user_id}/body/log/weight/goal.json") 43 | end 44 | 45 | # Get a list of logged body fat entries for the specified period 46 | # 47 | # @params [Hash] opts body fat options 48 | # @option opts [Date] date The date in the format YYYY-mm-dd. 49 | # @option opts [Date] base_date The end date when period is provided, in the 50 | # format yyyy-MM-dd or today; range start date when a date range is provided. 51 | # @option opts [String] period The date range period. One of 1d, 7d, 30d, 1w, 1m 52 | # @option opts [Date] end_date Range end date when date range is provided. 53 | # Note that period should not be longer than 31 day 54 | # 55 | # @return [Hash] A hash containing body fat log entries 56 | # 57 | # @since v0.9.0 58 | def body_fat(opts = {}) 59 | get determine_body_uri("/user/#{@user_id}/body/log/fat", opts) 60 | end 61 | 62 | # Retrieve the body fat goal of the current user 63 | # 64 | # @return [Hash] A hash containing the body fat goal 65 | # 66 | # @since v0.9.0 67 | def body_fat_goal 68 | get("/user/#{@user_id}/body/log/fat/goal.json") 69 | end 70 | 71 | # ========================================== 72 | # Body Measurements Update Methods 73 | # ========================================== 74 | 75 | # Log body measurements to fitbit for the current user 76 | # 77 | # At least ONE measurement item is REQUIRED in the call, as well as the 78 | # date. All measurement values to be logged for the user must be either an 79 | # Integer, a Decimal value, or a String in "X.XX" format. The 80 | # measurement units used for the supplied measurements are based on 81 | # which {Fitgem::ApiUnitSystem} is set in {Fitgem::Client#api_unit_system}. 82 | # 83 | # @param [Hash] opts The options including data to log for the user 84 | # @option opts [Integer, Decimal, String] :weight Weight measurement 85 | # @option opts [Integer, Decimal, String] :waist Waist measurement 86 | # @option opts [Integer, Decimal, String] :thigh Thigh measurement 87 | # @option opts [Integer, Decimal, String] :neck Neck measurement 88 | # @option opts [Integer, Decimal, String] :hips Hips measurement 89 | # @option opts [Integer, Decimal, String] :forearm Forearm measurement 90 | # @option opts [Integer, Decimal, String] :fat Body fat percentage measurement 91 | # @option opts [Integer, Decimal, String] :chest Chest measurement 92 | # @option opts [Integer, Decimal, String] :calf Calf measurement 93 | # @option opts [Integer, Decimal, String] :bicep Bicep measurement 94 | # @option opts [DateTime, Date, String] :date Date to log measurements 95 | # for; provided either as a DateTime, Date, or a String in 96 | # "yyyy-MM-dd" format 97 | # 98 | # @return [Hash] Hash containing the key +:body+ with an inner hash 99 | # of all of the logged measurements 100 | # 101 | # @since v0.4.0 102 | def log_body_measurements(opts) 103 | # Update the date (if exists) 104 | opts[:date] = format_date(opts[:date]) if opts[:date] 105 | post("/user/#{@user_id}/body.json", opts) 106 | end 107 | 108 | # Log weight to fitbit for the current user 109 | # 110 | # @param [Integer, String] weight The weight to log, as either 111 | # an integer or a string in "X.XX'" format 112 | # @param [DateTime, String] date The date the weight should be 113 | # logged, as either a DateTime or a String in 'yyyy-MM-dd' format 114 | # 115 | # @options opts [DateTime, String] :time The time the weight should be logged 116 | # as either a DateTime or a String in 'HH:mm:ss' format 117 | # 118 | # @return [Hash] 119 | # 120 | # @since v0.9.0 121 | def log_weight(weight, date, opts={}) 122 | opts[:time] = format_time(opts[:time]) if opts[:time] 123 | post("/user/#{@user_id}/body/log/weight.json", opts.merge(:weight => weight, :date => format_date(date))) 124 | end 125 | 126 | # Log body fat percentage 127 | # 128 | # @param [Decimal, Integer, String] fatPercentage Body fat percentage to log, 129 | # in format "X.XX" if a string 130 | # @param [DateTime, Date, String] date The date to log body fat percentage on, 131 | # in format "yyyy-MM-dd" if a string 132 | # @param [Hash] opts 133 | # @option opts [DateTime, Time, String] :time The time to log body fat percentage 134 | # at, in " HH:mm:ss" format if a String 135 | # 136 | # @since v0.9.0 137 | def log_body_fat(fatPercentage, date, opts={}) 138 | opts[:fat] = fatPercentage 139 | opts[:date] = format_date(date) 140 | opts[:time] = format_time(opts[:time]) if opts[:time] 141 | post("/user/#{@user_id}/body/fat.json", opts) 142 | end 143 | 144 | # Create or update a user's weight goal 145 | # 146 | # @param [DateTime, Date, String] startDate Weight goal start date; 147 | # in the format "yyyy-MM-dd" if a string 148 | # @param [Decimal, Integer, String] startWeight Weight goal start weight; 149 | # in the format "X.XX" if a string 150 | # @param [Decimal, Integer, String] goalWeight Weight goal target weight; 151 | # in the format "X.XX" if a string 152 | # 153 | # @since v0.9.0 154 | def create_or_update_body_weight_goal(startDate, startWeight, goalWeight) 155 | opts = {startDate: format_date(startDate), startWeight: startWeight, weight: goalWeight} 156 | post("/user/#{@user_id}/body/log/weight/goal.json", opts) 157 | end 158 | 159 | # Create or update a user's body fat percentage goal 160 | # 161 | # @param [Decimal, Integer, String] fatPercentageGoal Target body fat in %; 162 | # in the format "X.XX" if a string 163 | # 164 | # @since v0.9.0 165 | def create_or_update_body_fat_goal(fatPercentageGoal) 166 | opts = {fat: fatPercentageGoal} 167 | post("/user/#{@user_id}/body/log/fat/goal.json", opts) 168 | end 169 | 170 | # ========================================== 171 | # Body Measurements Delete Methods 172 | # ========================================== 173 | 174 | # Delete user's body weight log entry with the given id 175 | # 176 | # @param [Integer] logId The id of the body weight entry 177 | # 178 | # @since v0.9.0 179 | def delete_body_weight_log(logId) 180 | delete("/user/#{@user_id}/body/log/weight/#{logId}.json") 181 | end 182 | 183 | # Delete user's body fat log entry with the given id 184 | # 185 | # @param [Integer] logId The id of the body fat entry 186 | # 187 | # @since v0.9.0 188 | def delete_body_fat_log(logId) 189 | delete("/user/#{@user_id}/body/log/fat/#{logId}.json") 190 | end 191 | 192 | private 193 | 194 | # Determine the URI for the body_weight or body_fat method 195 | # 196 | # @params [String] base_uri the base URI for the body weight or body fat method 197 | # @params [Hash] opts body weight/fat options 198 | # @option opts [Date] date The date in the format YYYY-mm-dd. 199 | # @option opts [Date] base-date The end date when period is provided, in the 200 | # format yyyy-MM-dd or today; range start date when a date range is provided. 201 | # @option opts [String] period The date range period. One of 1d, 7d, 30d, 1w, 1m 202 | # @option opts [Date] end-date Range end date when date range is provided. 203 | # Note that period should not be longer than 31 day 204 | # 205 | # @return [String] an URI based on the base URI and provided options 206 | def determine_body_uri(base_uri, opts = {}) 207 | if opts[:date] 208 | date = format_date opts[:date] 209 | "#{base_uri}/date/#{date}.json" 210 | elsif opts[:base_date] && (opts[:period] || opts[:end_date]) 211 | date_range = construct_date_range_fragment opts 212 | "#{base_uri}/#{date_range}.json" 213 | else 214 | raise Fitgem::InvalidArgumentError, "You didn't supply one of the required options." 215 | end 216 | end 217 | end 218 | end 219 | -------------------------------------------------------------------------------- /spec/fitgem_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Fitgem::Client do 4 | before(:each) do 5 | @client = Fitgem::Client.new({ 6 | :consumer_key => '12345', 7 | :consumer_secret => '67890' 8 | }) 9 | end 10 | 11 | describe "#construct_date_range_fragment" do 12 | it 'should format the correct URI fragment based on a base date and end date' do 13 | frag = @client.construct_date_range_fragment({:base_date => '2011-03-07', :end_date => '2011-03-14'}) 14 | expect(frag).to eq 'date/2011-03-07/2011-03-14' 15 | end 16 | 17 | it 'should format the correct URI fragment based on a base date and period' do 18 | frag = @client.construct_date_range_fragment({:base_date => '2011-03-07', :period => '7d'}) 19 | expect(frag).to eq 'date/2011-03-07/7d' 20 | end 21 | 22 | it 'should raise an error unless there is a base date AND either a period or an end date' do 23 | expect { 24 | @client.construct_date_range_fragment({:base_date => '2011-03-07'}) 25 | }.to raise_error(Fitgem::InvalidTimeRange) 26 | 27 | expect { 28 | @client.construct_date_range_fragment({:period => '1y'}) 29 | }.to raise_error(Fitgem::InvalidTimeRange) 30 | 31 | expect { 32 | @client.construct_date_range_fragment({:end_date => '2011-03-07', :period => '7d'}) 33 | }.to raise_error(Fitgem::InvalidTimeRange) 34 | end 35 | 36 | it 'formats DateTime objects to the required date format' do 37 | span_start = DateTime.new(1997, 4, 18, 9, 30, 30) 38 | span_end = DateTime.new(1999, 10, 3, 15, 30, 30) 39 | frag = @client.construct_date_range_fragment base_date: span_start, end_date: span_end 40 | expect(frag).to eq 'date/1997-04-18/1999-10-03' 41 | end 42 | end 43 | 44 | describe "#format_date" do 45 | it "accepts DateTime objects" do 46 | date = DateTime.strptime('2011-03-19','%Y-%m-%d') 47 | expect(@client.format_date(date)).to eq '2011-03-19' 48 | end 49 | 50 | it "accepts strings in YYYY-MM-DD format" do 51 | expect(@client.format_date('2011-03-19')).to eq '2011-03-19' 52 | end 53 | 54 | it "accepts the string 'today' to denote the current date" do 55 | today = Date.today.strftime("%Y-%m-%d") 56 | expect(@client.format_date('today')).to eq today 57 | end 58 | 59 | it "accepts the string 'yesterday' to denote the day previous to today" do 60 | yesterday = (Date.today-1).strftime("%Y-%m-%d") 61 | expect(@client.format_date('yesterday')).to eq yesterday 62 | end 63 | 64 | it "rejects strings that are not in YYY-MM-DD format" do 65 | date = "2011-3-2" 66 | expect { 67 | @client.format_date(date) 68 | }.to raise_error Fitgem::InvalidDateArgument, "Invalid date (2011-3-2), must be in yyyy-MM-dd format" 69 | end 70 | 71 | it "rejects strings are formatted correctly but include non-numeric elements" do 72 | date = "2011-aa-20" 73 | expect { 74 | @client.format_date(date) 75 | }.to raise_error Fitgem::InvalidDateArgument, "Invalid date (2011-aa-20), must be in yyyy-MM-dd format" 76 | end 77 | 78 | it "rejects arguments that are not a Date, Time, DateTime, or String" do 79 | date = 200 80 | expect { 81 | @client.format_date(date) 82 | }.to raise_error Fitgem::InvalidDateArgument, "Date used must be a date/time object or a string in the format YYYY-MM-DD; supplied argument is a Fixnum" 83 | end 84 | end 85 | 86 | describe "#format_time" do 87 | context "without a timezone" do 88 | it "accepts DateTime objects" do 89 | time = DateTime.parse("3rd Feb 2001 04:05:06 PM") 90 | expect(@client.format_time(time)).to eq "16:05" 91 | end 92 | 93 | it "accepts Time objects" do 94 | time = Time.mktime 2012, 1, 20, 13, 33, 30 95 | expect(@client.format_time(time)).to eq "13:33" 96 | end 97 | 98 | it "accepts the string 'now' to denote the current localtime" do 99 | now = DateTime.now 100 | expect(@client.format_time('now')).to eq now.strftime("%H:%M") 101 | end 102 | 103 | it "accepts strings in HH:mm format" do 104 | time = "04:20" 105 | expect(@client.format_time(time)).to eq "04:20" 106 | end 107 | end 108 | 109 | context "with a timezone" do 110 | it "accepts DateTime objects" do 111 | time = DateTime.parse("3rd Feb 2001 04:05:06 PM UTC") 112 | expect(@client.format_time(time, include_timezone: true)).to eq "16:05+00:00" 113 | end 114 | 115 | it "accepts Time objects" do 116 | time = Time.new 2012, 1, 20, 13, 33, 30, "+00:00" 117 | expect(@client.format_time(time, include_timezone: true)).to eq "13:33+00:00" 118 | end 119 | 120 | it "accepts the string 'now' to denote the current localtime" do 121 | now = DateTime.now 122 | expect(@client.format_time('now', include_timezone: true)).to eq now.strftime("%H:%M%:z") 123 | end 124 | 125 | it "accepts strings in HH:mm format" do 126 | datetime = DateTime.parse("26th Apr 2000 09:27:00 +08:00") 127 | allow(DateTime).to receive(:now).and_return datetime 128 | 129 | time = "04:20" 130 | expect(@client.format_time(time, include_timezone: true)).to eq "04:20+08:00" 131 | end 132 | end 133 | 134 | it "rejects Date objects" do 135 | date = Date.today 136 | expect { 137 | @client.format_time(date) 138 | }.to raise_error Fitgem::InvalidTimeArgument 139 | end 140 | 141 | it "rejects strings that are not in HH:mm format" do 142 | time = "4:20pm" 143 | expect { 144 | @client.format_time(time) 145 | }.to raise_error Fitgem::InvalidTimeArgument 146 | end 147 | 148 | it "rejects strings that are formatted correctly but include non-numeric elements" do 149 | time = "4a:33" 150 | expect { 151 | @client.format_time(time) 152 | }.to raise_error Fitgem::InvalidTimeArgument 153 | end 154 | 155 | it "rejects arguments that do not conform to accepted types" do 156 | expect { 157 | @client.format_time(200) 158 | }.to raise_error Fitgem::InvalidTimeArgument 159 | end 160 | end 161 | 162 | describe "#label_for_measurement" do 163 | it "accepts the supported Fitgem::ApiUnitSystem values" do 164 | @client.api_unit_system = Fitgem::ApiUnitSystem.US 165 | @client.label_for_measurement :duration, false 166 | @client.api_unit_system = Fitgem::ApiUnitSystem.UK 167 | @client.label_for_measurement :duration, false 168 | @client.api_unit_system = Fitgem::ApiUnitSystem.METRIC 169 | @client.label_for_measurement :duration, false 170 | end 171 | 172 | it "raises an InvalidUnitSystem error if the Fitgem::Client.api_unit_system value is invalid" do 173 | expect { 174 | @client.api_unit_system = "something else entirely" 175 | @client.label_for_measurement :duration, false 176 | }.to raise_error Fitgem::InvalidUnitSystem 177 | end 178 | 179 | it "accepts the supported values for the measurement_type parameter" do 180 | @client.label_for_measurement :duration, false 181 | @client.label_for_measurement :distance, false 182 | @client.label_for_measurement :elevation, false 183 | @client.label_for_measurement :height, false 184 | @client.label_for_measurement :weight, false 185 | @client.label_for_measurement :measurements, false 186 | @client.label_for_measurement :liquids, false 187 | @client.label_for_measurement :blood_glucose, false 188 | end 189 | 190 | it "raises an InvalidMeasurementType error if the measurement_type parameter is invalid" do 191 | expect { 192 | @client.label_for_measurement :homina, false 193 | }.to raise_error Fitgem::InvalidMeasurementType 194 | end 195 | 196 | it "returns the correct values when the unit system is Fitgem::ApiUnitSystem.US" do 197 | @client.api_unit_system = Fitgem::ApiUnitSystem.US 198 | expect(@client.label_for_measurement(:duration, false)).to eq "milliseconds" 199 | expect(@client.label_for_measurement(:distance, false)).to eq "miles" 200 | expect(@client.label_for_measurement(:elevation, false)).to eq "feet" 201 | expect(@client.label_for_measurement(:height, false)).to eq "inches" 202 | expect(@client.label_for_measurement(:weight, false)).to eq "pounds" 203 | expect(@client.label_for_measurement(:measurements, false)).to eq "inches" 204 | expect(@client.label_for_measurement(:liquids, false)).to eq "fl oz" 205 | expect(@client.label_for_measurement(:blood_glucose, false)).to eq "mg/dL" 206 | end 207 | 208 | it "returns the correct values when the unit system is Fitgem::ApiUnitSystem.UK" do 209 | @client.api_unit_system = Fitgem::ApiUnitSystem.UK 210 | expect(@client.label_for_measurement(:duration, false)).to eq "milliseconds" 211 | expect(@client.label_for_measurement(:distance, false)).to eq "kilometers" 212 | expect(@client.label_for_measurement(:elevation, false)).to eq "meters" 213 | expect(@client.label_for_measurement(:height, false)).to eq "centimeters" 214 | expect(@client.label_for_measurement(:weight, false)).to eq "stone" 215 | expect(@client.label_for_measurement(:measurements, false)).to eq "centimeters" 216 | expect(@client.label_for_measurement(:liquids, false)).to eq "mL" 217 | expect(@client.label_for_measurement(:blood_glucose, false)).to eq "mmol/l" 218 | end 219 | 220 | it "returns the correct values when the unit system is Fitgem::ApiUnitSystem.METRIC" do 221 | @client.api_unit_system = Fitgem::ApiUnitSystem.METRIC 222 | expect(@client.label_for_measurement(:duration, false)).to eq "milliseconds" 223 | expect(@client.label_for_measurement(:distance, false)).to eq "kilometers" 224 | expect(@client.label_for_measurement(:elevation, false)).to eq "meters" 225 | expect(@client.label_for_measurement(:height, false)).to eq "centimeters" 226 | expect(@client.label_for_measurement(:weight, false)).to eq "kilograms" 227 | expect(@client.label_for_measurement(:measurements, false)).to eq "centimeters" 228 | expect(@client.label_for_measurement(:liquids, false)).to eq "mL" 229 | expect(@client.label_for_measurement(:blood_glucose, false)).to eq "mmol/l" 230 | end 231 | 232 | context "when respecting the user's unit measurement preferences" do 233 | before(:each) do 234 | allow(@client).to receive(:connected?).and_return(true) 235 | allow(@client).to receive(:user_info).and_return({"user" => {"distanceUnit"=>"en_GB", "glucoseUnit"=>"en_GB", "heightUnit"=>"en_GB", "waterUnit"=>"METRIC", "weightUnit"=>"en_GB"}}) 236 | end 237 | 238 | it "returns the correct overridden measurement label" do 239 | @client.api_unit_system = Fitgem::ApiUnitSystem.US 240 | expect(@client.label_for_measurement(:distance)).to eq "kilometers" 241 | end 242 | end 243 | end 244 | end 245 | -------------------------------------------------------------------------------- /lib/fitgem/activities.rb: -------------------------------------------------------------------------------- 1 | module Fitgem 2 | class Client 3 | # ========================================== 4 | # Activity Retrieval Methods 5 | # ========================================== 6 | 7 | # Get a list of all activities 8 | # 9 | # The returned list includes all public activities plus any 10 | # activities that the current user created and logged 11 | # 12 | # @return [Hash] Hash tree of all activities 13 | def activities 14 | get("/activities.json") 15 | end 16 | 17 | # Get all of the logged activities on the supplied date 18 | # 19 | # @param [Datetime] date the date to retrieve logged activities for 20 | # @return [Hash] the activities logged on the supplied date 21 | def activities_on_date(date) 22 | get("/user/#{@user_id}/activities/date/#{format_date(date)}.json") 23 | end 24 | 25 | # Get activity information for a date range 26 | # 27 | # @param [String] activity to retrieve information for 28 | # @param [Datetime] start of date range 29 | # @param [Datetime] end of date range 30 | # @return [Hash] activity information for the given date range 31 | def activity_on_date_range(activity, start, finish) 32 | get("/user/#{@user_id}/activities/#{activity}/date/#{format_date(start)}/#{format_date(finish)}.json") 33 | end 34 | 35 | # Get a list of the frequently logged activities 36 | # 37 | # @return [Array] list of frequently logged activities 38 | def frequent_activities 39 | get("/user/#{@user_id}/activities/frequent.json") 40 | end 41 | 42 | # Get a list of recently logged activities 43 | # 44 | # @return [Array] list of of recently-logged activities 45 | def recent_activities 46 | get("/user/#{@user_id}/activities/recent.json") 47 | end 48 | 49 | # Get a list of activities marked as favorite by the user 50 | # 51 | # @return [Array] list of user-defined favorite activities 52 | def favorite_activities 53 | get("/user/#{@user_id}/activities/favorite.json") 54 | end 55 | 56 | # Get the detailed information for a single activity 57 | # 58 | # @param [Integer, String] id The ID of the activity. This ID is 59 | # typcially retrieved by browsing or searching the list of all 60 | # available activities. Must be a numerical ID in either Integer 61 | # or String format. 62 | # @return [Hash] a hash structure containing detailed information 63 | # about the activity 64 | def activity(id) 65 | get("/activities/#{id}.json") 66 | end 67 | 68 | # Get the activity statistics for the current user 69 | # 70 | # @return [Hash] Hash containing statistics for the activities that 71 | # the user has logged 72 | def activity_statistics 73 | get("/user/#{@user_id}/activities.json") 74 | end 75 | 76 | # ========================================== 77 | # Goal Retrieval Methods 78 | # ========================================== 79 | 80 | # Get the daily activity goals for the current user 81 | # 82 | # @return [Hash] Hash containing the calorie, distance, floors, and 83 | # step goals for the current user 84 | # 85 | # @deprecated Use the daily_goals method; in v1.0.0 this method will 86 | # be removed 87 | def goals 88 | get("/user/#{@user_id}/activities/goals/daily.json") 89 | end 90 | 91 | # Get the daily activity goals for the current user 92 | # 93 | # @return [Hash] Hash containing the calorie, distance, floors, and 94 | # step goals for the current user 95 | # 96 | # @since v0.9.0 97 | def daily_goals 98 | goals 99 | end 100 | 101 | # Get the weekly activity goals for the current user 102 | # 103 | # @return [Hash] Hash containing the distance, floors and step goals 104 | # for the current user 105 | def weekly_goals 106 | get("/user/#{@user_id}/activities/goals/weekly.json") 107 | end 108 | 109 | # ========================================== 110 | # Activity Update Methods 111 | # ========================================== 112 | 113 | # Log an activity to fitbit 114 | # 115 | # @param [Hash] opts The options used to log the activity for the 116 | # current user 117 | # 118 | # @option opts [String] :activityId The id of the activity, directory 119 | # activity or intensity level activity. See activity types at 120 | # {http://wiki.fitbit.com/display/API/API-Log-Activity} for more 121 | # information. This value OR activityName is REQUIRED for all calls. 122 | # @option opts [String] :activityName The name of the activity to log. 123 | # This value OR activityId is REQUIRED for alls. 124 | # @option opts [String, Integer] :durationMillis Activity duration in milliseconds. 125 | # Must be a numeric value in Integer or String format. This value is REQUIRED for all calls. 126 | # @option opts [String] :startTime Activity start time, in the format "HH:mm" using hours 127 | # and seconds. This value is REQUIRED for all calls. 128 | # @option opts [String] :date Activity date, in "yyyy-MM-dd" format. This 129 | # value is REQUIRED for all calls. 130 | # @option opts [String] :distance Distance traveled, a string in the format "X.XX". This value is 131 | # REQUIRED when logging a directory activity, OPTIONAL otherwise. 132 | # @option opts [String, Integer] :manualCalories The number of calories 133 | # to log against the activity. This value is REQUIRED if using the activityName, 134 | # OPTIONAL otherwise. 135 | # @option opts [String] :distanceUnit One of {Fitgem::ApiDistanceUnit}. 136 | # The "steps" units are available only for "Walking" and "Running" 137 | # directory activities and their intensity levels 138 | # 139 | # @return [Hash] A hash with a summary of the logged activity 140 | def log_activity(opts) 141 | post("/user/#{@user_id}/activities.json", opts) 142 | end 143 | 144 | # Mark an activity as a favorite 145 | # 146 | # @param [String, Integer] :activity_id The ID of the activity. This ID is 147 | # typcially retrieved by browsing or searching the list of all 148 | # available activities. Must be a numerical ID in either Integer 149 | # or String format. 150 | # 151 | # @return [Hash] Empty hash if successfully marked as a favorite 152 | def add_favorite_activity(activity_id) 153 | post("/user/#{@user_id}/activities/log/favorite/#{activity_id}.json") 154 | end 155 | 156 | # ========================================== 157 | # Goal Creation/Update Methods 158 | # ========================================== 159 | 160 | # Create or update a user's daily goal 161 | # 162 | # @param [Hash] :opts 163 | # 164 | # @option opts [Symbol] :type The type of goal to create or update; must be one of 165 | # :steps, :distance, :floors, or :caloriesOut. REQUIRED 166 | # @option opts [Decimal, Integer, String] :value The goal value; in the format 'X.XX' 167 | # if a string. REQUIRED 168 | # 169 | # @since v0.9.0 170 | def create_or_update_daily_goal(opts) 171 | unless opts[:type] && [:steps, :distance, :floors, :caloriesOut].include?(opts[:type]) 172 | raise InvalidArgumentError, 'Must specify type in order to create or update a daily goal. One of (:steps, :distance, :floors, or :caloriesOut) is required.' 173 | end 174 | 175 | unless opts[:value] 176 | raise InvalidArgumentError, 'Must specify value of the daily goal to be created or updated.' 177 | end 178 | 179 | post("/user/#{@user_id}/activities/goals/daily.json", opts) 180 | end 181 | 182 | # Create or update a user's weekly goal 183 | # 184 | # @param [Hash] :opts 185 | # 186 | # @option opts [Symbol] :type The type of goal to create or update; must be one of 187 | # :steps, :distance, or :floors. REQUIRED 188 | # @option opts [Decimal, Integer, String] :value The goal value; in the format 'X.XX' 189 | # if a string. REQUIRED 190 | # 191 | # @since v0.9.0 192 | def create_or_update_weekly_goal(opts) 193 | unless opts[:type] && [:steps, :distance, :floors].include?(opts[:type]) 194 | raise InvalidArgumentError, 'Must specify type in order to create or update a weekly goal. One of (:steps, :distance, or :floors) is required.' 195 | end 196 | 197 | unless opts[:value] 198 | raise InvalidArgumentError, 'Must specify value of the weekly goal to be created or updated.' 199 | end 200 | 201 | post("/user/#{@user_id}/activities/goals/weekly.json", opts) 202 | end 203 | 204 | # ========================================== 205 | # Activity Removal Methods 206 | # ========================================== 207 | 208 | # Delete a logged activity 209 | # 210 | # @param [String, Integer] :activity_log_id The ID of a previously 211 | # logged activity. Note that this is NOT the activity ID itself, but 212 | # the ID of the logging of the activity (returned when you call 213 | # {#log_activity}). 214 | # @return [Hash] 215 | def delete_logged_activity(activity_log_id) 216 | delete("/user/#{@user_id}/activities/#{activity_log_id}.json") 217 | end 218 | 219 | # Unmark an activity as a favorite 220 | # 221 | # @param [String, Integer] :activity_id The activity ID of the 222 | # activity to remove as a favorite. 223 | # @return [Hash] 224 | def remove_favorite_activity(activity_id) 225 | delete("/user/#{@user_id}/activities/log/favorite/#{activity_id}.json") 226 | end 227 | 228 | # ========================================== 229 | # Intraday Activity Data Methods 230 | # ========================================== 231 | 232 | # Get intraday time series data for the specified resource 233 | # 234 | # @option opts [Symbol] :resource The sub-resource you want to retrieve time series data for; must 235 | # be one of :calories, :steps, :floors, or :elevation. This value is REQUIRED for all calls. 236 | # @option opts [String, Datetime, Date] :date The date to retrieve a time series on. This value is 237 | # REQUIRED for all calls. 238 | # @option opts [String] :detailLevel The resolution of the time series data; must be one of "1min" 239 | # or "15min". 240 | # 241 | # @options opts [DateTime, Time, String] :startTime The start of the time period to request data for. 242 | # @options opts [DateTime, Time, String] :endTime The end of the time period to request data for. 243 | # 244 | # @return [Hash] A hash containing time series data 245 | def intraday_time_series(opts) 246 | unless opts[:resource] && [:calories, :steps, :distance, :floors, :elevation].include?(opts[:resource]) 247 | raise Fitgem::InvalidArgumentError, 'Must specify resource to fetch intraday time series data for. One of (:calories, :steps, :distance, :floors, or :elevation) is required.' 248 | end 249 | 250 | unless opts[:date] 251 | raise Fitgem::InvalidArgumentError, 'Must specify the date to fetch intraday time series data for.' 252 | end 253 | 254 | unless opts[:detailLevel] && %w(1min 15min).include?(opts[:detailLevel]) 255 | raise Fitgem::InvalidArgumentError, 'Must specify the data resolution to fetch intraday time series data for. One of (\"1d\" or \"15min\") is required.' 256 | end 257 | 258 | resource = opts.delete(:resource) 259 | date = format_date(opts.delete(:date)) 260 | detail_level = opts.delete(:detailLevel) 261 | time_window_specified = opts[:startTime] || opts[:endTime] 262 | resource_path = "/user/#{@user_id}/activities/" 263 | 264 | if time_window_specified 265 | start_time = format_time(opts.delete(:startTime)) 266 | end_time = format_time(opts.delete(:endTime)) 267 | resource_path += "#{resource}/date/#{date}/1d/#{detail_level}/time/#{start_time}/#{end_time}.json" 268 | else 269 | resource_path += "#{resource}/date/#{date}/1d/#{detail_level}.json" 270 | end 271 | 272 | get(resource_path, opts) 273 | end 274 | end 275 | end 276 | --------------------------------------------------------------------------------