diff --git a/.Rbuildignore b/.Rbuildignore index b28703c..da6311f 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -15,3 +15,4 @@ ^CONDUCT.*$ ^CODE.*$ ^\.gitlab-ci\.yml$ +^LICENSE\.md$ diff --git a/DESCRIPTION b/DESCRIPTION index f4d1a68..6c2bf47 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: deere Type: Package -Title: deere title goes here otherwise CRAN checks fail +Title: Catchall Functions for All Things 'John Deere' Version: 0.1.0 Date: 2019-06-10 Authors@R: c( @@ -8,18 +8,20 @@ Authors@R: c( comment = c(ORCID = "0000-0001-5670-2640")) ) Maintainer: Bob Rudis -Description: A good description goes here otherwise CRAN checks fail. +Description: Initially a convenience package to access 'John Deere' 'MowerPlus' databases + from 'iOS' backups but perpaps will be something more all-encompassing. URL: https://gitlab.com/hrbrmstr/deere BugReports: https://gitlab.com/hrbrmstr/deere/issues Encoding: UTF-8 -License: AGPL +License: MIT + file LICENSE Suggests: testthat, covr Depends: R (>= 3.2.0) Imports: - httr, - jsonlite + tibble, + dplyr, + RSQLite Roxygen: list(markdown = TRUE) RoxygenNote: 6.1.1 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b6a10f1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,2 @@ +YEAR: 2019 +COPYRIGHT HOLDER: Bob Rudis diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..c36552c --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +# MIT License + +Copyright (c) 2019 Bob Rudis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/NAMESPACE b/NAMESPACE index 5b4b9ae..d32e89a 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,4 +1,7 @@ # Generated by roxygen2: do not edit by hand -import(httr) -importFrom(jsonlite,fromJSON) +export(from_coredata_ts) +export(src_mowerplus) +import(tibble) +importFrom(dplyr,src_sqlite) +importFrom(dplyr,tbl) diff --git a/R/deere-package.R b/R/deere-package.R index ff6c707..f2783c7 100644 --- a/R/deere-package.R +++ b/R/deere-package.R @@ -1,12 +1,21 @@ -#' ... -#' +#' Catchall Functions for All Things 'John Deere' +#' +#' Initially a convenience package to access 'John Deere' 'MowerPlus' databases from +#' 'iOS' backups but perpaps will be something more all-encompassing. +#' +#' Ref: +#' +#' - +#' - +#' +#' #' - URL: #' - BugReports: -#' +#' #' @md #' @name deere -#' @docType package +#' @keywords internal #' @author Bob Rudis (bob@@rud.is) -#' @import httr -#' @importFrom jsonlite fromJSON -NULL +#' @import tibble +#' @importFrom dplyr tbl src_sqlite +"_PACKAGE" diff --git a/R/src_mowerplus.R b/R/src_mowerplus.R new file mode 100644 index 0000000..9a468da --- /dev/null +++ b/R/src_mowerplus.R @@ -0,0 +1,52 @@ +#' Find and sync a copy of the latest MowerPlus database file from an iOS backup +#' +#' @md +#' @note You may need to [setup permissions](https://rud.is/b/2019/06/02/trawling-through-ios-backups-for-treasure-a-k-a-how-to-fish-for-target-files-in-ios-backups-with-r/) +#' to be able to use this method depending on which macOS version you're on. +#' @param backup_id the giant hex string of a folder name +#' @param data_loc where `mowtrack.sqlite` will be sync'd +#' @param overwrite nuke ^^ if present (def: `TRUE`) +#' @export +#' @examples \dontrun{ +#' mow_db <- src_mowerplus("28500cd31b9580aaf5815c695ebd3ea5f7455628") +#' +#' mow_db +#' +#' glimpse(tbl(mow_db, "ZMOWER")) +#' +#' glimpse(tbl(mow_db, "ZACTIVITY")) +#' +#' } +src_mowerplus <- function(backup_id, data_loc = "~/Data", overwrite = TRUE) { + + # root of mobile backup dir for `backup_id` + mb <- path.expand(file.path("~/Library/Application Support/MobileSync/Backup", backup_id)) + stopifnot(dir.exists(mb)) + + data_loc <- path.expand(data_loc) + stopifnot(dir.exists(data_loc)) + + tf <- tempfile(fileext = ".sqlite") + on.exit(unlink(tf), add=TRUE) + + # path to the extracted sqlite file + out_db <- file.path(data_loc, "mowtrack.sqlite") + + file.copy(file.path(mb, "Manifest.db"), tf, overwrite = TRUE) + + manifest_db <- src_sqlite(tf) + + fils <- tbl(manifest_db, "Files") + + filter(fils, relativePath == "Library/Application Support/MowTracking.sqlite") %>% + pull(fileID) -> mowtrackdb_loc + + file.copy( + file.path(mb, sprintf("%s/%s", substr(mowtrackdb_loc, 1, 2), mowtrackdb_loc)), + file.path(data_loc, "mowtrack.sqlite"), + overwrite = overwrite + ) + + src_sqlite(out_db) + +} diff --git a/R/utils.R b/R/utils.R new file mode 100644 index 0000000..3806115 --- /dev/null +++ b/R/utils.R @@ -0,0 +1,14 @@ +#' Convert timestampes from Apple "CoreData" format to something usable +#' +#' @md +#' @param x timestamps in Apple "CoreData" format(dates or times) +#' @param tz passed on to the convertion to a `POSIXct` object. Def: `NULL`. +#' @export +from_coredata_ts <- function(x, tz = NULL) { + .POSIXct(ifelse( + test = floor(log10(x)) >= 10, # If you're still using R in 2317 then good on ya and edit this + yes = as.POSIXct(x/10e8, origin = "2001-01-01"), # nanoseconds coredata + no = as.POSIXct(x, origin = "2001-01-01") # seconds coredata + ), tz = tz) +} + diff --git a/README.Rmd b/README.Rmd index 64ba007..f4c2b4e 100644 --- a/README.Rmd +++ b/README.Rmd @@ -4,7 +4,7 @@ editor_options: chunk_output_type: inline --- ```{r pkg-knitr-opts, include=FALSE} -knitr$opts_chunk$set(collapse=TRUE, fig.retina=2, message=FALSE, warning=FALSE) +knitr::opts_chunk$set(collapse=TRUE, fig.retina=2, message=FALSE, warning=FALSE) options(width=120) ``` @@ -14,8 +14,17 @@ options(width=120) # deere +Catchall Functions for All Things 'John Deere' + ## Description +Initially a convenience package to access 'John Deere' 'MowerPlus' databases from 'iOS' backups but perpaps will be something more all-encompassing. + +Ref: + +- +- + ## What's Inside The Tin The following functions are implemented: @@ -38,12 +47,91 @@ devtools::install_github("hrbrmstr/deere") ```{r lib-ex} library(deere) +library(hrbrthemes) +library(tidyverse) # current version packageVersion("deere") ``` +### Sample from a recent mow + +```{r mow, cache=TRUE} +mow_db <- src_mowerplus("28500cd31b9580aaf5815c695ebd3ea5f7455628") + +mow_db + +glimpse(tbl(mow_db, "ZMOWER")) + +glimpse(tbl(mow_db, "ZACTIVITY")) + +tbl(mow_db, "ZACTIVITY")%>% + collect() -> activity + +activity %>% + select( + mow_date = ZCREATEDAT, + area_covered = ZAREACOVERED, + avg_speed = ZAVERAGESPEED, + distance = ZDISTANCEMOWED, + duration = ZMOWINGTIME + ) %>% + arrange(mow_date) %>% + mutate( + duration = duration / 60 / 60, # hours + mow_date = format(from_coredata_ts(mow_date), "%b %d"), # factors make better bars + mow_date = factor(mow_date, levels = unique(mow_date)) # when there are just 2-of-em + ) %>% + gather(measure, value, -mow_date) %>% + ggplot(aes(mow_date, value)) + + geom_col(aes(fill = measure), width = 0.5, show.legend = FALSE) + + scale_y_comma() + + scale_fill_ipsum() + + facet_wrap(~measure, scales = "free") + + theme_ipsum_rc(grid="Y") + +zloc <- tbl(mow_db, "ZMOWLOCATION") + + +zloc %>% + select( + id = ZSESSION, + zorder = ZORDER, + lat = ZLATITUDE, + lng = ZLONGITUDE, + speed = ZSPEED, + ts = ZTIMESTAMP + ) %>% + collect() %>% + mutate( + id = factor(id), + ts = from_coredata_ts(ts) + ) -> sessions + +ggplot(sessions, aes(id, speed)) + + ggbeeswarm::geom_quasirandom( + aes(fill = id), show.legend = FALSE, + shape = 21, size = 2, color = "white", stroke = 0.75 + ) + + scale_fill_ipsum() + + labs(x = "Mowing Session", y = "MPH", title = "Mowing Speed Comparison (mph)") + + theme_ipsum_rc(grid="Y") + +arrange(sessions, ts) %>% + ggplot(aes(lng, lat)) + + geom_path( + aes(color = id, group = id), show.legend = FALSE, + size = 1, alpha = 1/2 + ) + + scale_color_ipsum() + + coord_quickmap() + + facet_wrap(~id) + + labs(title = "Mowing Path Comparison") + + theme_ipsum_rc(grid="Y") + + ggthemes::theme_map() +``` + ## deere Metrics ```{r cloc, echo=FALSE} diff --git a/README.md b/README.md index 10ba81c..fb688e0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,205 @@ + +[![Travis-CI Build +Status](https://travis-ci.org/hrbrmstr/deere.svg?branch=master)](https://travis-ci.org/hrbrmstr/deere) +[![Coverage +Status](https://codecov.io/gh/hrbrmstr/deere/branch/master/graph/badge.svg)](https://codecov.io/gh/hrbrmstr/deere) +[![CRAN\_Status\_Badge](http://www.r-pkg.org/badges/version/deere)](https://cran.r-project.org/package=deere) + # deere +Catchall Functions for All Things ‘John Deere’ + +## Description + +Initially a convenience package to access ‘John Deere’ ‘MowerPlus’ +databases from ‘iOS’ backups but perpaps will be something more +all-encompassing. + +Ref: + + - + - + +## What’s Inside The Tin + +The following functions are implemented: + +## Installation + +``` r +devtools::install_git("https://git.sr.ht/~hrbrmstr/deere.git") +# or +devtools::install_git("https://git.rud.is/hrbrmstr/deere.git") +# or +devtools::install_gitlab("hrbrmstr/deere") +# or +devtools::install_bitbucket("hrbrmstr/deere") +# or +devtools::install_github("hrbrmstr/deere") +``` + +## Usage + +``` r +library(deere) +library(hrbrthemes) +library(tidyverse) + +# current version +packageVersion("deere") +## [1] '0.1.0' +``` + +### Sample from a recent mow + +``` r +mow_db <- src_mowerplus("28500cd31b9580aaf5815c695ebd3ea5f7455628") + +mow_db +## src: sqlite 3.22.0 [/Users/hrbrmstr/Data/mowtrack.sqlite] +## tbls: Z_METADATA, Z_MODELCACHE, Z_PRIMARYKEY, ZACTIVITY, ZDEALER, ZMOWALERT, ZMOWER, ZMOWLOCATION, ZSMARTCONNECTOR, +## ZUSER + +glimpse(tbl(mow_db, "ZMOWER")) +## Observations: ?? +## Variables: 23 +## Database: sqlite 3.22.0 [/Users/hrbrmstr/Data/mowtrack.sqlite] +## $ Z_PK 1 +## $ Z_ENT 7 +## $ Z_OPT 11 +## $ ZDECKSIZEINCHES 48 +## $ ZDISMISSEDFULLSERVICETASK 0 +## $ ZDISMISSEDPERIODICTASK 0 +## $ ZSMARTCONNECTOR NA +## $ ZUSER 1 +## $ ZBATTERYCHARGE NA +## $ ZENGINEHOURS 3.474705 +## $ ZFULLSERVICEPERFORMED NA +## $ ZHMCLASTSEEN NA +## $ ZHMCOFFSET 0 +## $ ZPERIODICSERVICEPERFORMED NA +## $ ZSCLASTCONNECTED NA +## $ ZGENERICTYPE NA +## $ ZHMCIDENTIFIER NA +## $ ZMODEL "E140" +## $ ZSCPIN NA +## $ ZSCPERIPHERALID NA +## $ ZSERIALNUMBER "1GXE140EKKK116940" +## $ ZSERIES "E100" +## $ ZSCDATADICTIONARY + +glimpse(tbl(mow_db, "ZACTIVITY")) +## Observations: ?? +## Variables: 20 +## Database: sqlite 3.22.0 [/Users/hrbrmstr/Data/mowtrack.sqlite] +## $ Z_PK 1, 2 +## $ Z_ENT 3, 3 +## $ Z_OPT 124, 93 +## $ ZMONTH 6, 6 +## $ ZYEAR 2019, 2019 +## $ ZMOWER 1, 1 +## $ ZUSER 1, 1 +## $ ZISCOMPLETE 1, 1 +## $ ZISMISSEDMOW 0, 0 +## $ ZLASTLOCATION 7016, 12548 +## $ ZCREATEDAT 581100260, 581778616 +## $ ZENGINEHOURS NA, NA +## $ ZAREACOVERED 3.761875, 2.286811 +## $ ZAVERAGESPEED 3.727754, 2.894269 +## $ ZDISTANCEMOWED 7.758894, 4.716564 +## $ ZMOWINGTIME 6960.000, 5548.939 +## $ ZNOTES "First mow!", NA +## $ ZINTERVALNAME NA, NA +## $ ZTYPE NA, NA +## $ ZUUID blob[238 B], blob[238 B] + +tbl(mow_db, "ZACTIVITY")%>% + collect() -> activity + +activity %>% + select( + mow_date = ZCREATEDAT, + area_covered = ZAREACOVERED, + avg_speed = ZAVERAGESPEED, + distance = ZDISTANCEMOWED, + duration = ZMOWINGTIME + ) %>% + arrange(mow_date) %>% + mutate( + duration = duration / 60 / 60, # hours + mow_date = format(from_coredata_ts(mow_date), "%b %d"), # factors make better bars + mow_date = factor(mow_date, levels = unique(mow_date)) # when there are just 2-of-em + ) %>% + gather(measure, value, -mow_date) %>% + ggplot(aes(mow_date, value)) + + geom_col(aes(fill = measure), width = 0.5, show.legend = FALSE) + + scale_y_comma() + + scale_fill_ipsum() + + facet_wrap(~measure, scales = "free") + + theme_ipsum_rc(grid="Y") +``` + + + +``` r + +zloc <- tbl(mow_db, "ZMOWLOCATION") + + +zloc %>% + select( + id = ZSESSION, + zorder = ZORDER, + lat = ZLATITUDE, + lng = ZLONGITUDE, + speed = ZSPEED, + ts = ZTIMESTAMP + ) %>% + collect() %>% + mutate( + id = factor(id), + ts = from_coredata_ts(ts) + ) -> sessions + +ggplot(sessions, aes(id, speed)) + + ggbeeswarm::geom_quasirandom( + aes(fill = id), show.legend = FALSE, + shape = 21, size = 2, color = "white", stroke = 0.75 + ) + + scale_fill_ipsum() + + labs(x = "Mowing Session", y = "MPH", title = "Mowing Speed Comparison (mph)") + + theme_ipsum_rc(grid="Y") +``` + + + +``` r + +arrange(sessions, ts) %>% + ggplot(aes(lng, lat)) + + geom_path( + aes(color = id, group = id), show.legend = FALSE, + size = 1, alpha = 1/2 + ) + + scale_color_ipsum() + + coord_quickmap() + + facet_wrap(~id) + + labs(title = "Mowing Path Comparison") + + theme_ipsum_rc(grid="Y") + + ggthemes::theme_map() +``` + + + +## deere Metrics + +| Lang | \# Files | (%) | LoC | (%) | Blank lines | (%) | \# Lines | (%) | +| :--- | -------: | ---: | --: | ---: | ----------: | ---: | -------: | ---: | +| Rmd | 1 | 0.17 | 74 | 0.69 | 31 | 0.69 | 39 | 0.45 | +| R | 5 | 0.83 | 33 | 0.31 | 14 | 0.31 | 48 | 0.55 | + +## Code of Conduct + +Please note that this project is released with a [Contributor Code of +Conduct](CONDUCT.md). By participating in this project you agree to +abide by its terms. diff --git a/README_files/figure-gfm/mow-1.png b/README_files/figure-gfm/mow-1.png new file mode 100644 index 0000000..616a06d Binary files /dev/null and b/README_files/figure-gfm/mow-1.png differ diff --git a/README_files/figure-gfm/mow-2.png b/README_files/figure-gfm/mow-2.png new file mode 100644 index 0000000..d8eb3c1 Binary files /dev/null and b/README_files/figure-gfm/mow-2.png differ diff --git a/README_files/figure-gfm/mow-3.png b/README_files/figure-gfm/mow-3.png new file mode 100644 index 0000000..5d5bdcd Binary files /dev/null and b/README_files/figure-gfm/mow-3.png differ diff --git a/man/deere.Rd b/man/deere.Rd index 23aea79..041c761 100644 --- a/man/deere.Rd +++ b/man/deere.Rd @@ -4,13 +4,29 @@ \name{deere} \alias{deere} \alias{deere-package} -\title{...} +\title{Catchall Functions for All Things 'John Deere'} \description{ +Initially a convenience package to access 'John Deere' 'MowerPlus' databases from +'iOS' backups but perpaps will be something more all-encompassing. +} +\details{ +Ref: \itemize{ +\item \url{https://rud.is/b/2019/06/02/trawling-through-ios-backups-for-treasure-a-k-a-how-to-fish-for-target-files-in-ios-backups-with-r/} +\item \url{https://rud.is/b/2019/06/09/wrapping-up-exploration-of-john-deeres-mowerplus-database/} \item URL: \url{https://gitlab.com/hrbrmstr/deere} \item BugReports: \url{https://gitlab.com/hrbrmstr/deere/issues} } } +\seealso{ +Useful links: +\itemize{ + \item \url{https://gitlab.com/hrbrmstr/deere} + \item Report bugs at \url{https://gitlab.com/hrbrmstr/deere/issues} +} + +} \author{ Bob Rudis (bob@rud.is) } +\keyword{internal} diff --git a/man/from_coredata_ts.Rd b/man/from_coredata_ts.Rd new file mode 100644 index 0000000..17d0cbd --- /dev/null +++ b/man/from_coredata_ts.Rd @@ -0,0 +1,16 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{from_coredata_ts} +\alias{from_coredata_ts} +\title{Convert timestampes from Apple "CoreData" format to something usable} +\usage{ +from_coredata_ts(x, tz = NULL) +} +\arguments{ +\item{x}{timestamps in Apple "CoreData" format(dates or times)} + +\item{tz}{passed on to the convertion to a \code{POSIXct} object. Def: \code{NULL}.} +} +\description{ +Convert timestampes from Apple "CoreData" format to something usable +} diff --git a/man/src_mowerplus.Rd b/man/src_mowerplus.Rd new file mode 100644 index 0000000..c276399 --- /dev/null +++ b/man/src_mowerplus.Rd @@ -0,0 +1,34 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/src_mowerplus.R +\name{src_mowerplus} +\alias{src_mowerplus} +\title{Find and sync a copy of the latest MowerPlus database file from an iOS backup} +\usage{ +src_mowerplus(backup_id, data_loc = "~/Data", overwrite = TRUE) +} +\arguments{ +\item{backup_id}{the giant hex string of a folder name} + +\item{data_loc}{where \code{mowtrack.sqlite} will be sync'd} + +\item{overwrite}{nuke ^^ if present (def: \code{TRUE})} +} +\description{ +Find and sync a copy of the latest MowerPlus database file from an iOS backup +} +\note{ +You may need to \href{https://rud.is/b/2019/06/02/trawling-through-ios-backups-for-treasure-a-k-a-how-to-fish-for-target-files-in-ios-backups-with-r/}{setup permissions} +to be able to use this method depending on which macOS version you're on. +} +\examples{ +\dontrun{ +mow_db <- src_mowerplus("28500cd31b9580aaf5815c695ebd3ea5f7455628") + +mow_db + +glimpse(tbl(mow_db, "ZMOWER")) + +glimpse(tbl(mow_db, "ZACTIVITY")) + +} +}