fitbit_client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FitbitClient
4
+ class Error < StandardError
5
+ attr_accessor :response
6
+
7
+ def initialize(message, response = nil)
8
+ @message = message
9
+ @response = response
10
+ end
11
+
12
+ def json_response
13
+ @json_response ||= JSON.parse(response.body) if response
14
+ end
15
+
16
+ def to_s
17
+ if response
18
+ "#{@message}, HTTP_STATUS: #{response.status}, HTTP_BODY: #{response.body}, HTTP_URL: #{response.response.env.url}"
19
+ else
20
+ @message.to_s
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FitbitClient
4
+ module Network
5
+ module Request
6
+ def get_json(path, params = {}, headers = {})
7
+ parse_response(get(path, params, headers))
8
+ end
9
+
10
+ def get(path, params = {}, headers = {})
11
+ request(:get, path, headers: headers, params: params)
12
+ end
13
+
14
+ def post_json(path, params = {}, headers = {})
15
+ parse_response(post(path, params, headers))
16
+ end
17
+
18
+ def post(path, params = {}, headers = {})
19
+ request(:post, path, headers: headers, params: params)
20
+ end
21
+
22
+ def delete_json(path, params = {}, headers = {})
23
+ parse_response(delete(path, params, headers))
24
+ end
25
+
26
+ def delete(path, params = {}, headers = {})
27
+ request(:delete, path, headers: headers, params: params)
28
+ end
29
+
30
+ def successful_post?(response)
31
+ response.status == 201 && response.body == '{}'
32
+ end
33
+
34
+ def successful_delete?(response)
35
+ response.status == 204 && response.body.empty?
36
+ end
37
+
38
+ private
39
+
40
+ def request(method, path, opts = {})
41
+ attempt = 0
42
+ begin
43
+ default_request_headers(opts)
44
+ oauth2_access_token.request(method, path, opts)
45
+ rescue OAuth2::Error => e # Handle refresh token issue automagically
46
+ attempt += 1
47
+ if expired_token_error?(e.response)
48
+ oauth2_refresh_token!
49
+ retry if attempt < 2
50
+ end
51
+ raise FitbitClient::Error.new('Error during OAuth2 request', e.response)
52
+ end
53
+ end
54
+
55
+ def default_request_headers(opts)
56
+ opts[:headers]['User-Agent'] = "FitbitClient v#{::FitbitClient::VERSION}"
57
+ opts[:headers]['Accept-Language'] = opts.fetch(:language, ::FitbitClient.default_language)
58
+ opts[:headers]['Accept-Locale'] = opts.fetch(:locale, ::FitbitClient.default_locale)
59
+ end
60
+
61
+ def parse_response(response)
62
+ return {} if response.nil? || response.body.nil? || response.body.empty?
63
+ JSON.parse response.body
64
+ end
65
+
66
+ def expired_token_error?(response)
67
+ json_response = parse_response(response)
68
+ json_response.dig('errors', 0, 'errorType') == 'expired_token'
69
+ end
70
+
71
+ def oauth2_refresh_token!
72
+ @oauth2_access_token = oauth2_access_token.refresh!
73
+ refresh_token_callback!(@oauth2_access_token)
74
+ rescue OAuth2::Error => e
75
+ raise FitbitClient::Error.new('Error during oauth2_refresh_token! attempt', e.response)
76
+ end
77
+
78
+ def oauth2_access_token
79
+ @oauth2_access_token ||= OAuth2::AccessToken.new(oauth2_client, access_token, refresh_token: refresh_token)
80
+ end
81
+
82
+ def oauth2_client
83
+ @oauth2_client ||= OAuth2::Client.new(client_id, client_secret, FitbitClient::OAUTH2_CLIENT_OPTIONS)
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FitbitClient
4
+ # This module is used to include all resources (endpoints)
5
+ module Resources
6
+ include FitbitClient::Util
7
+ include FitbitClient::Network::Request
8
+ include FitbitClient::Resources::Common
9
+ include FitbitClient::Resources::Activity
10
+ include FitbitClient::Resources::BodyAndWeight
11
+ include FitbitClient::Resources::Devices
12
+ include FitbitClient::Resources::Sleep
13
+ include FitbitClient::Resources::Subscription
14
+ end
15
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FitbitClient
4
+ module Resources
5
+ module Activity
6
+ # The Get Lifetime Stats endpoint retrieves the user's activity statistics
7
+ # in the format requested using units in the unit system which corresponds
8
+ # to the Accept-Language header provided.
9
+ #
10
+ # Activity statistics includes Lifetime and Best achievement values
11
+ # from the My Achievements tile on the website dashboard.
12
+ #
13
+ # Response contains both statistics from the tracker device and
14
+ # total numbers including tracker data and manual activity log entries
15
+ # as seen on the Fitbit website dashboard.
16
+ def lifetime_stats(options = {})
17
+ get_json(path_user_version('/activities', options))
18
+ end
19
+
20
+ # The Get Daily Activity Summary endpoint retrieves a summary and list
21
+ # of a user's activities and activity log entries for a given day in the
22
+ # format requested using units in the unit system which corresponds to
23
+ # the Accept-Language header provided.
24
+ def daily_activity_summary(date, options = {})
25
+ get_json(path_user_version("/activities/date/#{iso_date(date)}", options))
26
+ end
27
+
28
+ # The Get Activity Time Series endpoint returns time series data in
29
+ # the specified range for a given resource in the format requested
30
+ # using units in the unit system that corresponds to
31
+ # the Accept-Language header provided.
32
+ def activity_time_series(resource, date, period_or_end_date, options = {})
33
+ period = period_or_date_param(period_or_end_date)
34
+ path = "/activities/#{resource}/date/#{iso_date(date)}/#{period}"
35
+ get_json(path_user_version(path, options))
36
+ end
37
+
38
+ # Get a tree of all valid Fitbit public activities from the activities
39
+ # catalog as well as private custom activities the user created in the
40
+ # format requested.
41
+ #
42
+ # If the activity has levels, also get a list of activity level details.
43
+ def browse_activity_types(options = {})
44
+ skip_user_options!(options)
45
+ get_json(path_user_version('/activities', options))
46
+ end
47
+
48
+ # Returns the details of a specific activity in the Fitbit activities
49
+ # database in the format requested. If activity has levels, also returns
50
+ # a list of activity level details.
51
+ #
52
+ # activity_id : The activity id
53
+ def activity_type(activity_id, options = {})
54
+ skip_user_options!(options)
55
+ get_json(path_user_version("/activities/#{activity_id}", options))
56
+ end
57
+
58
+ # The Get Recent Activity Types endpoint retrieves a list of a user's
59
+ # recent activities types logged with some details of the last activity
60
+ # log of that type using units in the unit system which corresponds to
61
+ # the Accept-Language header provided.
62
+ #
63
+ # The record retrieved can be used to log the activity via the
64
+ # Log Activity endpoint with the same or adjusted values for distance
65
+ # and duration.
66
+ def recent_activity_types(options = {})
67
+ get_json(path_user_version('/activities/recent', options))
68
+ end
69
+
70
+ # The Get Favorite Activities endpoint returns a list of a user's
71
+ # favorite activities.
72
+ def favorite_activities
73
+ get_json(path_user_version('/activities/favorite'))
74
+ end
75
+
76
+ # The Add Favorite Activity endpoint adds the activity with the given ID
77
+ # to user's list of favorite activities.
78
+ #
79
+ # activity_id : The ID of the activity to add to user's favorites.
80
+ def add_favorite_activity(activity_id)
81
+ successful_post?(post(path_user_version("/activities/favorite/#{activity_id}")))
82
+ end
83
+
84
+ # The Delete Favorite Activity removes the activity with the given ID
85
+ # from a user's list of favorite activities.
86
+ #
87
+ # activity_id : The ID of the activity to be removed.
88
+ def delete_favorite_activity(activity_id)
89
+ successful_delete?(delete(path_user_version("/activities/favorite/#{activity_id}")))
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FitbitClient
4
+ module Resources
5
+ module BodyAndWeight
6
+ # The Get Weight Logs API retrieves a list of all user's body weight log
7
+ # entries for a given day using units in the unit systems which
8
+ # corresponds to the Accept-Language header provided.
9
+ #
10
+ # Body weight log entries are available only to authorized user.
11
+ #
12
+ # Body weight log entries in response are sorted exactly the same as they
13
+ # are presented on the Fitbit website.
14
+ def weight_logs(date, _options = {})
15
+ get_json(path_user_version("/body/log/weight/date/#{iso_date(date)}"))
16
+ end
17
+
18
+ # The Log Weight API creates log entry for a body weight using units in
19
+ # the unit systems that corresponds to the Accept-Language header provided
20
+ # and get a response in the format requested.
21
+ #
22
+ # <b>Note:</b> The returned Weight Log IDs are unique to the user,
23
+ # but not globally unique.
24
+ #
25
+ # weight : Weight in the format X.XX
26
+ # date : Date of the measurement
27
+ # time : Time of the measurement
28
+ def log_weight(weight, date, time = nil, options = {})
29
+ params = key_value_date_time_params('weight', weight, date, time)
30
+ post_json(path_user_version('/body/log/weight', options), params)
31
+ end
32
+
33
+ # The Delete Weight Log API deletes a user's body weight log entry with
34
+ # the given ID.
35
+ def delete_weight_log(body_weight_log_id, options = {})
36
+ path = "/body/log/weight/#{body_weight_log_id}"
37
+ successful_delete?(delete(path_user_version(path, options)))
38
+ end
39
+
40
+ # Retrieves a user's current body fat percentage or weight goal using
41
+ # units in the unit systems that corresponds to the Accept-Language header
42
+ # provided in the format requested.
43
+ #
44
+ # goal_type : can be weight or fat
45
+ def body_goals(goal_type, options = {})
46
+ get_json(path_user_version("/body/log/#{goal_type}/goal", options))
47
+ end
48
+
49
+ # The Update Body Fat Goal API creates or updates user's fat percentage
50
+ # goal.
51
+ #
52
+ # fat : Target body fat percentage in the format X.XX
53
+ def update_body_fat_goal(fat, options = {})
54
+ post_json(path_user_version('/body/log/fat/goal', options), 'fat' => fat)
55
+ end
56
+
57
+ # The Update Weight Goal API creates or updates user's fat or weight goal
58
+ # using units in the unit systems that corresponds to the Accept-Language
59
+ # header provided in the format requested.
60
+ #
61
+ # startDate : Weight goal start date
62
+ # startWeight : Weight goal start weight; in the format X.XX
63
+ # weight : Weight goal target weight; in the format X.XX, in the
64
+ # unit systems that corresponds to the Accept-Language
65
+ # header provided; required if user doesn't have an existing
66
+ # weight goal.
67
+ def update_body_weight_goal(start_date, start_weight, weight = nil, options = {})
68
+ params = update_body_weight_goal_params(start_date, start_weight, weight)
69
+ post_json(path_user_version('/body/log/weight/goal', options), params)
70
+ end
71
+
72
+ # The Get Body Time Series API returns time series data in the specified
73
+ # range for a given resource in the format requested using units in the
74
+ # unit systems that corresponds to the Accept-Language header provided.
75
+ #
76
+ # <b>Note:</b> If you provide earlier dates in the request, the response
77
+ # retrieves only data since the user's join date or the first log entry
78
+ # date for the requested collection.
79
+ #
80
+ # resource : Can be "bmi", "fat", or "weight".
81
+ # date : The range start date or end date of the period specified
82
+ # period_or_end_date : One of 1d, 7d, 30d, 1w, 1m, 3m, 6m, 1y, max or the end date of the range
83
+ def body_time_series(resource, date, period_or_end_date, options = {})
84
+ end_limit = period_or_date_param(period_or_end_date)
85
+ path = time_series_path('body', resource, date, end_limit)
86
+ get_json(path_user_version(path, options))
87
+ end
88
+
89
+ # The Get Body Fat Logs API retrieves a list of all user's body fat log
90
+ # entries for a given day in the format requested. Body fat log entries
91
+ # are available only to authorized user.
92
+ #
93
+ # If you need to fetch only the most recent entry, you can use the
94
+ # Get Body Measurements endpoint.
95
+ #
96
+ # date : base or start date
97
+ # period_or_end_date : One of 1d, 7d, 1w, 1m or a Date object
98
+ #
99
+ # <b>Note:</b> The range should not be longer than 31 days.
100
+ def body_fat_logs(date, period_or_end_date = nil, options = {})
101
+ if period_or_end_date
102
+ end_limit = period_or_date_param(period_or_end_date)
103
+ path = "/body/log/fat/date/#{iso_date(date)}/#{end_limit}"
104
+ else
105
+ path = "/body/log/fat/date/#{iso_date(date)}"
106
+ end
107
+ get_json(path_user_version(path, options))
108
+ end
109
+
110
+ # The Log Body Fat API creates a log entry for body fat and returns a
111
+ # response in the format requested.
112
+ #
113
+ # <b>Note:</b> The returned Body Fat Log IDs are unique to the user, but
114
+ # not globally unique.
115
+ #
116
+ # fat : Body fat; in the format X.XX
117
+ # date : Log entry date
118
+ # time : Time of the measurement
119
+ def log_body_fat(fat, date, time = nil, options = {})
120
+ params = key_value_date_time_params('fat', fat, date, time)
121
+ post_json(path_user_version('/body/log/fat', options), params)
122
+ end
123
+
124
+ # The Delete Body Fat Log API deletes a user's body fat log entry with
125
+ # the given ID.
126
+ def delete_body_fat_log(body_fat_log_id, options = {})
127
+ successful_delete?(delete(path_user_version("/body/log/fat/#{body_fat_log_id}", options)))
128
+ end
129
+
130
+ private
131
+
132
+ def key_value_date_time_params(key, value, date, time)
133
+ params = { key => value, 'date' => iso_date(date) }
134
+ params['time'] = iso_time_with_seconds(time) if time
135
+ params
136
+ end
137
+
138
+ def update_body_weight_goal_params(start_date, start_weight, weight)
139
+ params = { 'startDate' => iso_date(start_date), 'startWeight' => start_weight }
140
+ params['weight'] = weight if weight
141
+ params
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FitbitClient
4
+ module Resources
5
+ module Common
6
+ def skip_user_options!(options)
7
+ options[:skip_user] = true
8
+ options.delete![:user_id] if options.key?(:user_id)
9
+ end
10
+
11
+ def time_series_path(type, resource, date, end_limit)
12
+ "/#{type}/#{resource}/date/#{iso_date(date)}/#{end_limit}"
13
+ end
14
+
15
+ def period_or_date_param(period_or_date)
16
+ period_or_date.is_a?(Date) ? iso_date(period_or_date) : period_or_date
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FitbitClient
4
+ module Resources
5
+ module Devices
6
+ # The Get Device endpoint returns a list of the Fitbit devices connected
7
+ # to a user's account.
8
+ #
9
+ # Third-party applications can check when a Fitbit device last synced
10
+ # with Fitbit's servers using this endpoint.
11
+ def devices(options = {})
12
+ get_json(path_user_version('/devices', options))
13
+ end
14
+
15
+ # Returns a list of the set alarms connected to a user's account.
16
+ # tracker_id : The ID of the tracker for which data is returned. The tracker-id value is found via the Get Devices endpoint.
17
+ def alarms(tracker_id, options = {})
18
+ get_json(path_user_version("/devices/tracker/#{tracker_id}/alarms", options))
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FitbitClient
4
+ module Resources
5
+ module Sleep
6
+ # The Get Sleep Time Series endpoint returns time series data in the
7
+ # specified range for a given resource in the format requested using units
8
+ # in the unit system that corresponds to the Accept-Language header provided.
9
+ #
10
+ # API Version: 1
11
+ #
12
+ # <b>Note:</b> This API has been <b>deprecated</b> with the introduction
13
+ # of version 1.2 of the \Sleep APIs described above. \Sleep Stages data
14
+ # cannot be retrieved with this API. If your application requires data
15
+ # consistency while you transition over to the version 1.2 \Sleep APIs,
16
+ # you can get this data through the version 1 Get Sleep Logs by Date
17
+ # endpoint.
18
+ #
19
+ # resource : startTime | timeInBed | minutesAsleep |
20
+ # awakeningsCount | minutesAwake | minutesToFallAsleep
21
+ # minutesAfterWakeup | efficiency
22
+ # date : The start date for a range or end date of the period specified
23
+ # period_or_end_date : One of 1d, 7d, 30d, 1w, 1m, 3m, 6m, 1y, max or the end date of the range
24
+ def sleep_time_series(resource, date, period_or_end_date, options = {})
25
+ end_limit = period_or_date_param(period_or_end_date)
26
+ path = time_series_path('sleep', resource, date, end_limit)
27
+ get_json(path_user_version(path, options))
28
+ end
29
+
30
+ # The Get Sleep Logs by Date endpoint returns a summary and list
31
+ # of a user's sleep log entries (including naps) as well as detailed sleep
32
+ # entry data for a given day.
33
+ #
34
+ # This endpoint supports two kinds of sleep data:
35
+ # - stages: Levels data is returned with 30-second granularity. 'Sleep Stages' levels include deep, light, rem, and wake.
36
+ # - classic: Levels data returned with 60-second granularity. 'Sleep Pattern' levels include asleep, restless, and awake.
37
+ #
38
+ # The response could be a mix of classic and stages sleep logs.
39
+ #
40
+ # <b>Note:</b> shortData is only included for stages sleep logs and
41
+ # includes wake periods that are 3 minutes or less in duration.
42
+ #
43
+ # This distinction is to simplify graphically distinguishing short wakes
44
+ # from longer wakes, but they are physiologically equivalent.
45
+ def sleep_logs_by_date(date, options = {})
46
+ path = "/sleep/date/#{iso_date(date)}"
47
+ sleep_default_version!(options)
48
+ get_json(path_user_version(path, options))
49
+ end
50
+
51
+ # The Get Sleep Logs by Date Range endpoint returns a list of a user's
52
+ # sleep log entries (including naps) as well as detailed sleep entry data
53
+ # for a given date range (inclusive of start and end dates).
54
+ #
55
+ # This endpoint supports two kinds of sleep data:
56
+ # - stages : Levels data is returned with 30-second granularity. 'Sleep Stages' levels include deep, light, rem, and wake.
57
+ # - classic : Levels data returned with 60-second granularity. 'Sleep Pattern' levels include asleep, restless, and awake.
58
+ #
59
+ # The response could be a mix of classic and stages sleep logs.
60
+ #
61
+ # <b>Note:</b> shortData is only included for stages sleep logs and
62
+ # includes wake periods that are 3 minutes or less in duration.
63
+ #
64
+ # This distinction is to simplify graphically distinguishing short wakes
65
+ # from longer wakes, but they are physiologically equivalent.
66
+ def sleep_logs_by_date_range(start_date, end_date, options = {})
67
+ path = "/sleep/date/#{iso_date(start_date)}/#{iso_date(end_date)}"
68
+ sleep_default_version!(options)
69
+ get_json(path_user_version(path, options))
70
+ end
71
+
72
+ # The Get Sleep Logs List endpoint returns a list of a user's sleep logs
73
+ # (including naps) before or after a given day with offset, limit, and sort order.
74
+ #
75
+ # This endpoint supports two kinds of sleep data:
76
+ # - stages : Levels data is returned with 30-second granularity. 'Sleep Stages' levels include deep, light, rem, and wake.
77
+ # - classic : Levels data returned with 60-second granularity. 'Sleep Pattern' levels include asleep, restless, and awake.
78
+ #
79
+ # The response could be a mix of classic and stages sleep logs.
80
+ #
81
+ # <b>Note:</b> shortData is only included for stages sleep logs and
82
+ # includes wake periods that are 3 minutes or less in duration.
83
+ #
84
+ # This distinction is to simplify graphically distinguishing short wakes
85
+ # from longer wakes, but they are physiologically equivalent.
86
+ #
87
+ # before_date : Either beforeDate or afterDate must be specified.
88
+ # Set sort to desc when using beforeDate.
89
+ # after_date : Either beforeDate or afterDate must be specified.
90
+ # Set sort to asc when using afterDate.
91
+ def sleep_logs_list(before_date, after_date, sort, limit, options = {})
92
+ raise 'Before date and Atfer date cannot both be specified' if before_date && after_date
93
+ if before_date
94
+ params = { 'beforeDate' => iso_date(before_date), 'sort' => sort, 'limit' => limit, 'offset' => 0 }
95
+ elsif after_date
96
+ params = { 'afterDate' => iso_date(after_date), 'sort' => sort, 'limit' => limit, 'offset' => 0 }
97
+ end
98
+ sleep_default_version!(options)
99
+ get_json(path_user_version('/sleep/list', options), params)
100
+ end
101
+
102
+ # The Log Sleep endpoint creates a log entry for a sleep event and
103
+ # returns a response in the format requested. Keep in mind that it is NOT
104
+ # possible to create overlapping log entries.
105
+ #
106
+ # The dateOfSleep in the response for the sleep log is the date on which
107
+ # the sleep event ends.
108
+
109
+ # It requires read & write access
110
+ def log_sleep(start_time, duration_milliseconds, date, options = {})
111
+ params = { 'date' => iso_date(date), 'startTime' => start_time.strftime('%H:%M'), 'duration' => duration_milliseconds }
112
+ sleep_default_version!(options)
113
+ post_json(path_user_version('/sleep'), params)
114
+ end
115
+
116
+ # The Delete Sleep Log endpoint deletes a user's sleep log entry with the
117
+ # given ID.
118
+ def delete_sleep_log(log_id)
119
+ successful_delete?(delete(path_user_version("/sleep/#{log_id}")))
120
+ end
121
+
122
+ # The Get Sleep Goal endpoint returns a user's current sleep goal using
123
+ # unit in the unit system that corresponds to the Accept-Language header
124
+ # provided in the format requested.
125
+ def sleep_goals
126
+ get_json(path_user_version('/sleep/goal'))
127
+ end
128
+
129
+ # Creates or updates a user's sleep goal and get a response in the in the
130
+ # format requested.
131
+ #
132
+ # Access Type: Read & Write
133
+ #
134
+ # min_duration_minutes : The target sleep duration is in minutes
135
+ def update_sleep_goals(min_duration_minutes)
136
+ post_json(path_user_version('/sleep/goal'), 'minDuration' => min_duration_minutes)
137
+ end
138
+
139
+ private
140
+
141
+ def sleep_default_version!(options)
142
+ if options[:version] == '1'
143
+ warn '[DEPRECATION] These endpoints are deprecated and support for them may end unexpectedly. If your application does not depend on the sleep as calculated by these endpoints, please use the new v1.2 sleep endpoints.'
144
+ else
145
+ options[:version] = '1.2'
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end