Skip to content

Commit

Permalink
Update iFitbit to v 0.1.8
Browse files Browse the repository at this point in the history
  • Loading branch information
bhelsel committed May 15, 2024
1 parent f559773 commit fd10f0a
Show file tree
Hide file tree
Showing 16 changed files with 110 additions and 44 deletions.
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import(ggplot2)
import(magrittr)
importFrom(DBI,dbConnect)
importFrom(DBI,dbDisconnect)
importFrom(DBI,dbExecute)
importFrom(DBI,dbExistsTable)
importFrom(DBI,dbReadTable)
importFrom(DBI,dbRemoveTable)
Expand Down
8 changes: 8 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# iFitbit 0.1.8
* Added `.adjustDates` and an overwrite argument to make continuous data collection easier and less memory intensive
* Added a database command within `DBI::dbExecute` to remove duplicate entries when writing to SQL database
* Updated `.readTables` to only include first 12 columns of sleep data as everyone in a study may not have sleep stages
* Updated `get_fitbit_device` to append last synced information to SQL database to provide a sync history
* Updated `get_fitbit_report` to include sleep data when exporting to the .xlsx format


# iFitbit 0.1.7
* Add `.readTables` database function.
* Add parallel processing to the `fitbit_heart_intraday` function.
Expand Down
12 changes: 10 additions & 2 deletions R/activity.R
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#' @param end.date The end date specified in the format YYYY-MM-DD or today, Default: Sys.Date()
#' @param returnData Return the data to the user's R environment, Default: TRUE
#' @param toSQL Write the data to a SQL database, Default: FALSE
#' @param overwrite Data extraction should be continued from the most recent date in the SQL database, Default: FALSE
#' @return Writes the activities to a SQL database stored in the data folder.
#' @details Extract calories, distance, elevation, physical activity, and steps from the Fitbit API.
#' @seealso
Expand All @@ -22,10 +23,14 @@

get_fitbit_activities <- function(token.pathname, resource = "All Resources",
start.date = Sys.Date(), end.date = Sys.Date(),
returnData = TRUE, toSQL = FALSE){
returnData = TRUE, toSQL = FALSE, overwrite = FALSE){

tkn <- .extract_token(token.pathname)

start.date <- .adjustDates(token.pathname, database = "activities", overwrite, start.date, end.date)$start.date

end.date <- .adjustDates(token.pathname, database = "activities", overwrite, start.date, end.date)$end.date

url_activity <- paste0("https://api.fitbit.com/1/", "user/", tkn$user, "/", "activities/")

# Check to see if data is greater than 100 days
Expand Down Expand Up @@ -125,7 +130,10 @@ get_fitbit_activities <- function(token.pathname, resource = "All Resources",
if(toSQL){
database <- .checkDatabase(tkn$directory, tkn$user)
con <- DBI::dbConnect(RSQLite::SQLite(), database)
DBI::dbWriteTable(con, "activities", data, overwrite = TRUE)
if(nrow(data) != 0){
DBI::dbExecute(con, sprintf("DELETE FROM %s WHERE date BETWEEN '%s' AND '%s'", "activities", data$date[1], data$date[nrow(data)]))
}
DBI::dbWriteTable(con, "activities", data, overwrite = overwrite, append = !overwrite)
DBI::dbDisconnect(con)
}

Expand Down
3 changes: 2 additions & 1 deletion R/database.R
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@
con <- DBI::dbConnect(RSQLite::SQLite(), file)
if(tableName %in% DBI::dbListTables(con)){
data <- DBI::dbReadTable(con, tableName)
main_data <- plyr::rbind.fill(main_data, data.frame(user_id, data))
if(tableName == "sleep") data <- data[, 1:12]
if(nrow(data) != 0) main_data <- plyr::rbind.fill(main_data, data.frame(user_id, data))
}
DBI::dbDisconnect(con)
}
Expand Down
3 changes: 2 additions & 1 deletion R/device.R
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ get_fitbit_device <- function(token.pathname, returnData = TRUE, toSQL = FALSE){
if(toSQL){
database <- .checkDatabase(tkn$directory, tkn$user)
con <- DBI::dbConnect(RSQLite::SQLite(), database)
DBI::dbWriteTable(con, "device", data, overwrite = TRUE)
DBI::dbExecute(con, sprintf("DELETE FROM %s WHERE DATE(lastSyncTime) = '%s'", "device", as.Date(data$lastSyncTime)))
DBI::dbWriteTable(con, "device", data, overwrite = FALSE, append = TRUE)
DBI::dbDisconnect(con)
}

Expand Down
5 changes: 4 additions & 1 deletion R/exercise.R
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,10 @@ get_fitbit_exercise_log <- function(token.pathname, after.date = Sys.Date(),


data <- data.frame(cbind(date, time, type, duration, steps, calories, hr, distance, distanceUnit, logType, activityLevel, heartZones))
data <- data[data$duration >= 1 & !is.na(data$duration), ]

# data <- data[data$duration >= 1 & !is.na(data$duration), ] # Removed this filter for now as I am not sure the best option

data <- dplyr::arrange(data, date, time)

if(toSQL){
database <- .checkDatabase(tkn$directory, tkn$user)
Expand Down
18 changes: 11 additions & 7 deletions R/heart.R
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#' rate data, Default: FALSE
#' @param returnData Return a summary of the intensity minutes to the user's R environment, Default: TRUE
#' @param toSQL Write a summary of the intensity minutes to a SQL database, Default: FALSE
#' @param overwrite Data extraction should be continued from the most recent date in the SQL database, Default: FALSE
#' @param verbose Print the progress of the Fitbit API heart rate data extraction and calculation of intensity minutes, Default: FALSE
#' @return A measure of day-level non-wear, sleep, sedentary time, and physical activity
#' @details This function retrieves a continuous measure of heart rate from the Fitbit API. The user has access to a variety of
Expand Down Expand Up @@ -51,18 +52,17 @@ get_fitbit_heart_intraday <- function(token.pathname, start.date = Sys.Date(),
intensity = NULL, intensity_labels = NULL,
heart_rate_method = 1, rest.hr = NULL, do.parallel = FALSE,
returnData = TRUE, returnRawData = FALSE,
toSQL = FALSE, verbose = FALSE){
toSQL = FALSE, overwrite = FALSE, verbose = FALSE){

if(!equals(length(intensity), length(intensity_labels))) {
stop("Heart intensities and their labels must be the same length")
}

tkn <- .extract_token(token.pathname)

# Device
device <- get_fitbit_device(token.pathname, returnData = TRUE)
lastSync <- as.POSIXct(device$lastSyncTime, format = "%Y-%m-%dT%H:%M:%OS")
if(end.date > Sys.Date()) end.date <- Sys.Date()
start.date <- .adjustDates(token.pathname, database = "heart", overwrite, start.date, end.date)$start.date

end.date <- .adjustDates(token.pathname, database = "heart", overwrite, start.date, end.date)$end.date

# Age
if(is.null(age)) age <- get_fitbit_profile(token.pathname)$user$age
Expand All @@ -83,7 +83,10 @@ get_fitbit_heart_intraday <- function(token.pathname, start.date = Sys.Date(),
if(toSQL){
database <- .checkDatabase(directory, tkn$user)
con <- DBI::dbConnect(RSQLite::SQLite(), database)
DBI::dbWriteTable(con, "heart", heart_data, overwrite = TRUE)
if(nrow(heart_data) != 0){
DBI::dbExecute(con, sprintf("DELETE FROM %s WHERE date BETWEEN '%s' AND '%s'", "heart", heart_data$date[1], heart_data$date[nrow(heart_data)]))
}
DBI::dbWriteTable(con, "heart", heart_data, overwrite = overwrite, append = !overwrite)
DBI::dbDisconnect(con)
}

Expand Down Expand Up @@ -191,6 +194,7 @@ calculate_heart_intensities <- function(heart_raw_data = NULL, sleep_data = NULL
if(heart_rate_method %in% 1:2 & is.null(intensity)) {
intensities <- c(0, 0.20, 0.30, 0.40, 0.60, 0.90, 1) # Heart Rate Reserve
heart_categories <- do.call("c", lapply(intensities, function(x) ((max.hr - rest.hr) * x) + rest.hr))
heart_categories[1] <- 0
}

if(heart_rate_method == 3 & is.null(intensity)) {
Expand Down Expand Up @@ -218,7 +222,7 @@ calculate_heart_intensities <- function(heart_raw_data = NULL, sleep_data = NULL
}

# Add Nonwear and Intensity
heart_raw_data$hrr.percent <- cut(heart_raw_data$hr, breaks = heart_categories, labels = intensity_labels, include.lowest = TRUE, right = FALSE)
heart_raw_data$hrr.percent <- cut(heart_raw_data$hr, breaks = heart_categories, labels = intensity_labels, include.lowest = TRUE, right = TRUE)
heart_raw_data$nonwear <- ifelse(heart_raw_data$hr == 0, 1, 0)
if(!is.null(sleep_data)) {
heart_raw_data$sleep <- ifelse(heart_raw_data$sleep == 1 & heart_raw_data$nonwear == 0, 1, 0)
Expand Down
25 changes: 25 additions & 0 deletions R/helpers.R
Original file line number Diff line number Diff line change
Expand Up @@ -92,5 +92,30 @@
}


#' @title .adjustHeartDates
#' @description Adjusts the start and end date for the `get_fitbit_heart_intraday` function
#' @param token.pathname Full pathname to the location of the access token
#' @param database Name of the database to retrieve the last date value (Choices: activity, heart, and sleep)
#' @param overwrite Data extraction should be continued from the most recent date in the heart SQL database
#' @param start.date The start date specified in the format YYYY-MM-DD or today, Default: Sys.Date()
#' @param end.date The end date specified in the format YYYY-MM-DD or today, Default: Sys.Date()
#' @return An updated start and end date for the `get_fitbit_heart_intraday` function
#' @details Adjusts the start and end date for the `get_fitbit_heart_intraday` function
#' @noRd
#' @keywords internal

.adjustDates <- function(token.pathname, database, overwrite = FALSE, start.date, end.date){
tkn <- .extract_token(token.pathname)
device <- get_fitbit_device(token.pathname, returnData = TRUE)
lastSync <- as.POSIXct(device$lastSyncTime, format = "%Y-%m-%dT%H:%M:%OS")
if(end.date > lastSync) end.date <- as.Date(lastSync) - 1
if(!overwrite){
con <- DBI::dbConnect(RSQLite::SQLite(), .checkDatabase(directory, tkn$user))
lastDate <- tail(DBI::dbReadTable(con, database)$date, 1)
DBI::dbDisconnect(con)
if(length(lastDate) != 0) start.date <- as.Date(lastDate) - 1
}
return(list(start.date = start.date, end.date = end.date))
}


2 changes: 1 addition & 1 deletion R/iFitbit.R
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
#' @import ggplot2
#' @importFrom jsonlite fromJSON
#' @importFrom httr content GET POST add_headers content_type oauth_endpoint oauth_app oauth2.0_token
#' @importFrom DBI dbConnect dbWriteTable dbReadTable dbExistsTable dbRemoveTable dbDisconnect
#' @importFrom DBI dbConnect dbWriteTable dbReadTable dbExistsTable dbExecute dbRemoveTable dbDisconnect
#' @importFrom RSQLite SQLite
#' @importFrom RCurl base64Encode
#' @importFrom utils modifyList tail menu
Expand Down
6 changes: 4 additions & 2 deletions R/report.R
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ get_fitbit_report <- function(
con <- DBI::dbConnect(RSQLite::SQLite(), database_pathname)
table_names <- DBI::dbListTables(con)
activities <- if("activities" %in% table_names) DBI::dbReadTable(con, "activities")
sleep <- if("sleep" %in% table_names) DBI::dbReadTable(con, "sleep")
exercise_log <- if("exercise_log" %in% table_names) DBI::dbReadTable(con, "exercise_log")
heart <- if("heart" %in% table_names) DBI::dbReadTable(con, "heart")
device <- if("device" %in% table_names) DBI::dbReadTable(con, "device")
Expand All @@ -63,14 +64,15 @@ get_fitbit_report <- function(
# Create a list from the data
if(toXLSX | returnData){
names(exercise_log)[2:ncol(exercise_log)] <- paste0("exercise_", names(exercise_log)[2:ncol(exercise_log)])
data <- list(device = device, activities = activities, exercise_log = exercise_log, heart = heart)
data <- list(device = device, activities = activities, sleep = sleep, exercise_log = exercise_log, heart = heart)
}

if(toXLSX){
wb <- openxlsx::createWorkbook()
sapply(c("Device", "Activities", "Exercise Log", "Heart"), function(x) openxlsx::addWorksheet(wb, sheetName = x))
sapply(c("Device", "Activities", "Sleep", "Exercise Log", "Heart"), function(x) openxlsx::addWorksheet(wb, sheetName = x))
openxlsx::writeData(wb, sheet = "Device", x = device)
openxlsx::writeData(wb, sheet = "Activities", x = activities)
openxlsx::writeData(wb, sheet = "Sleep", x = sleep)
openxlsx::writeData(wb, sheet = "Exercise Log", x = exercise_log)
openxlsx::writeData(wb, sheet = "Heart", x = heart)
openxlsx::saveWorkbook(wb, paste0(reports_pathname, "/", user, ".xlsx"), overwrite = TRUE, ...)
Expand Down
35 changes: 18 additions & 17 deletions R/sleep.R
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#' @param end.date The end date specified in the format YYYY-MM-DD or today, Default: Sys.Date()
#' @param returnData Return the data to the user's R environment, Default: TRUE
#' @param toSQL Write the data to a SQL database, Default: FALSE
#' @param overwrite Data extraction should be continued from the most recent date in the SQL database, Default: FALSE
#' @return Output is either written to a SQL database or returned as a dataframe
#' @details Extract sleep outcomes including duration, efficiency, time
#' asleep, time awake, minutes to fall asleep, and time in bed.
Expand All @@ -23,14 +24,17 @@
#' @export

get_fitbit_sleep <- function(token.pathname, start.date = Sys.Date(),
end.date = Sys.Date(), returnData = TRUE, toSQL = FALSE){
end.date = Sys.Date(), returnData = TRUE,
toSQL = FALSE, overwrite = FALSE){

directory <- dirname(dirname(token.pathname))
token <- readRDS(token.pathname)
token <- token[[2]]
user <- token$credentials$user_id
sleep.url <- paste0("https://api.fitbit.com/1.2/user/", user, sprintf("/sleep/date/%s/%s.json", start.date, end.date))
sleep <- jsonlite::fromJSON(httr::content(httr::GET(sleep.url, token), as = "text"))
tkn <- .extract_token(token.pathname)

start.date <- .adjustDates(token.pathname, database = "sleep", overwrite, start.date, end.date)$start.date

end.date <- .adjustDates(token.pathname, database = "sleep", overwrite, start.date, end.date)$end.date

sleep.url <- paste0("https://api.fitbit.com/1.2/user/", tkn$user, sprintf("/sleep/date/%s/%s.json", start.date, end.date))
sleep <- jsonlite::fromJSON(httr::content(httr::GET(sleep.url, tkn$token), as = "text"))
data <- data.frame(matrix(ncol = 12, nrow = 0))
cols <- c("dateOfSleep", "startTime", "endTime", "isMainSleep", "duration", "efficiency", "minutesAfterWakeup", "minutesAsleep", "minutesAwake", "minutesToFallAsleep", "timeInBed", "type")
colnames(data) <- cols
Expand All @@ -52,21 +56,18 @@ get_fitbit_sleep <- function(token.pathname, start.date = Sys.Date(),
stages[, grep("thirtyDayAvg", colnames(stages))] <- NULL

data <- cbind(data, stages)

data <- dplyr::arrange(data, "dateOfSleep")

}

if(toSQL){
database <- grep(user, list.files(paste0(directory, "/data"), full.names = TRUE), value = TRUE)
database <- .checkDatabase(tkn$directory, tkn$user)
con <- DBI::dbConnect(RSQLite::SQLite(), database)
if(length(sleep$sleep)!=0){
DBI::dbWriteTable(con, "sleep", data, overwrite = TRUE)
}
if(length(sleep$sleep)==0 & DBI::dbExistsTable(con, "sleep")){
DBI::dbRemoveTable(con, "sleep")
if(nrow(data) != 0){
DBI::dbExecute(con, sprintf("DELETE FROM %s WHERE dateOfSleep BETWEEN '%s' AND '%s'", "sleep", data$dateOfSleep[1], data$dateOfSleep[nrow(data)]))
}
if(length(sleep$sleep)==0 & !DBI::dbExistsTable(con, "sleep")){
DBI::dbWriteTable(con, "sleep", data, overwrite = TRUE)
}

DBI::dbWriteTable(con, "sleep", data, overwrite = overwrite, append = !overwrite)
DBI::dbDisconnect(con)
}

Expand Down
2 changes: 1 addition & 1 deletion inst/rmd/BriefReport.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ hr.light {
```{r load libraries, include=FALSE}
library(ggplot2)
lastSynced <- as.POSIXct(device$lastSyncTime, format = "%Y-%m-%dT%H:%M:%OS", tz = "America/Chicago")
lastSynced <- as.POSIXct(device$lastSyncTime[nrow(device)], format = "%Y-%m-%dT%H:%M:%OS", tz = "America/Chicago")
```

Expand Down
21 changes: 12 additions & 9 deletions inst/rmd/FitbitReport.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,14 @@ library(ggplot2)

```{r deviceData, fig.height=1, fig.width=4, fig.align='center'}
deviceInfo <- device[nrow(device), ]
batteryColor <- dplyr::case_when(as.numeric(device$batteryLevel) >= 50 ~ "#39ff14",
as.numeric(device$batteryLevel) >= 20 & as.numeric(device$batteryLevel) <= 49 ~ "#ff6700",
as.numeric(device$batteryLevel) <= 19 ~ "#de1738")
batteryColor <- dplyr::case_when(as.numeric(deviceInfo$batteryLevel) >= 50 ~ "#39ff14",
as.numeric(deviceInfo$batteryLevel) >= 20 & as.numeric(deviceInfo$batteryLevel) <= 49 ~ "#ff6700",
as.numeric(deviceInfo$batteryLevel) <= 19 ~ "#de1738")
ggplot(data = device, aes(y="Battery Level")) +
ggplot(data = deviceInfo, aes(y="Battery Level")) +
geom_col(aes(x=as.numeric(batteryLevel)), fill = batteryColor) +
geom_col(aes(x = 100), alpha = 0.1, color = "black", fill = "white") +
geom_text(
Expand All @@ -124,7 +125,7 @@ ggplot(data = device, aes(y="Battery Level")) +
panel.background = element_blank())
lastSynced <- as.POSIXct(device$lastSyncTime, format = "%Y-%m-%dT%H:%M:%OS", tz = "America/Chicago")
lastSynced <- as.POSIXct(deviceInfo$lastSyncTime, format = "%Y-%m-%dT%H:%M:%OS", tz = "America/Chicago")
```

Expand Down Expand Up @@ -301,8 +302,10 @@ activities %>%
heart.img <- magick::image_read(system.file("images/heart.jpeg", package = "iFitbit"))
lastThree <- c((nrow(exercise_log) - 2):nrow(exercise_log))
# Most recent session is equal to 1
exercise_log_plot <- function(session=1){
exercise_log_plot <- function(session){
ggplot() +
geom_rect(aes(xmin = 0, xmax = 500, ymin = 0, ymax = 500), fill = "white", color = "black", size = 2) +
annotation_raster(heart.img, xmin = 300, xmax = 450, ymin = 150, ymax = 400) +
Expand All @@ -316,9 +319,9 @@ exercise_log_plot <- function(session=1){
}
gridExtra::grid.arrange(
exercise_log_plot(1),
exercise_log_plot(2),
exercise_log_plot(3),
exercise_log_plot(lastThree[1]),
exercise_log_plot(lastThree[2]),
exercise_log_plot(lastThree[3]),
nrow = 1
)
Expand Down
5 changes: 4 additions & 1 deletion man/get_fitbit_activities.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions man/get_fitbit_heart_intraday.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion man/get_fitbit_sleep.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit fd10f0a

Please sign in to comment.