Работа с API Google Fit с помощью пакета Go «fitness»

API Google Fit может быть немного запутанным вначале, и во время изучения его API я столкнулся со многими проблемами и недостатком информации. В этой статье я хочу поделиться своими решениями и привести несколько примеров, как получить данные из Google Fit.

Первый шаг: Включите API

Объяснение включения API в Google Cloud довольно длинное, поэтому я предлагаю вам прочитать официальную статью от Google: Включить API Google Fit

Второй шаг: Создайте проект

Создайте проект, который использует включенный Google Fit API. Также лучше прочитать это: https://cloud.google.com/resource-manager/docs/creating-managing-projects.

Извините, что вначале ссылаюсь на множество других источников, но потом станет легче.

Третий шаг: Создайте oAuth ClientID

Вы можете найти кнопку Create Credentials в Credentials вашего нового проекта. Так как мы будем работать с API с нашего локалхоста, в Authorised redirect URIs мы добавим URI: http://127.0.0.1. После этого создаются ClientID и ClientSecret. Они понадобятся в следующих шагах.

Не сообщайте свой ClientSecret. С его помощью можно подключиться к вашему API.

Создание HTTP-сервиса

Для простоты и понимания того, какие данные мы будем получать, давайте создадим небольшой HTTP-сервис, используя один из популярных HTTP веб-фреймворков Gin Gonic.

Структура нашего проекта будет выглядеть следующим образом:

main.go — это наш основной файл, в котором мы запускаем все наши маршруты.

func main() {
    log.Println("Server started. Press Ctrl-C to stop server")
    controllers.RunAllRoutes()
}
Вход в полноэкранный режим Выйти из полноэкранного режима

В controllers у нас есть 2 файла:

В .secrets должно быть 2 файла с форматом .dat. Именно там нам нужны наши скопированные ClientID и ClientSecret. Здесь мы просто вставим каждый из них в эти 2 файла.

В models у нас есть 2 файла:

В google-api я использовал пример пакета Go Fitness, где я всегда ссылался на fitness.go, скопировал файл debug.go и некоторые части main.go.

Самая важная часть файла — это авторизация клиента с помощью токена Google:

// authClient returns HTTP client using Google token from cache or web
func authClient(ctx context.Context, config *oauth2.Config) *http.Client {
    cacheFile := tokenCacheFile(config)
    token, err := tokenFromFile(cacheFile)
    if err != nil {
        token = tokenFromWeb(ctx, config)
        saveToken(cacheFile, token)
    } else {
        log.Printf("Using cached token %#v from %q", token, cacheFile)
    }

    return config.Client(ctx, token)
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Либо токен Google хранится в .secret в виде файла, либо пользователь авторизуется через браузер и затем токен сохраняется в .secret:

  • get.go состоит из функций, которые посылают запросы на получение определенных данных (гидратация, шаги, вес и т.д.) различными способами, которые я объясню позже в этой статье.

  • parse.go разбирает различные наборы данных в зависимости от типа данных.

Получение данных из Google Fit API

Более подробную информацию об этом API вы можете найти в официальной документации Google Fit API.

Пакет Go fitness — очень полезный инструмент, который мы будем использовать в этот раз.

Давайте рассмотрим два способа получения данных.

Агрегация данных

Если данные записываются непрерывно, Google Fit может агрегировать их, вычисляя средние значения или суммируя данные.

В качестве примера мы хотим получить агрегированные данные о весе.
В google-api/get.go напишем функцию GetDatasetBody. Она действительно может получать данные не только о весе, но и о росте.

func GetDatasetBody(ctx context.Context, startTime, endTime time.Time, dataType string) (*fitness.AggregateResponse, error) {
    ...
}
Вход в полноэкранный режим Выход из полноэкранного режима

Переменные startTime и endTime необходимы для запроса на указание периода записей. Я так и не нашел информации о том, насколько длинным может быть этот период. В моих случаях я буду запрашивать данные за последние 30 дней. dataType для этой функции может быть вес или рост.

Сначала нам нужно указать области действия. Поскольку данные о весе — это тип данных о теле, и мы хотим только прочитать данные, а не записывать их, область видимости будет https://www.googleapis.com/auth/fitness.body.read. Подробнее о диапазонах вы можете узнать здесь: https://developers.google.com/fit/datatypes и https://developers.google.com/fit/datatypes/aggregate.

Затем, запустив функцию authClient, авторизуем пользователя, используя кэшированный токен Google, или, если его нет, даем согласие на чтение данных тела.

func GetDatasetBody(ctx context.Context, startTime, endTime time.Time, dataType string) (*fitness.AggregateResponse, error) {
    flag.Parse()
    config, err := returnConfig([]string{
        fitness.FitnessBodyReadScope,
    })
    if err != nil {
        log.Println("returnConfig error", err.Error())
        return nil, err
    }

    if *debug {
        ctx = context.WithValue(ctx, oauth2.HTTPClient, &http.Client{
            Transport: &logTransport{http.DefaultTransport},
        })
    }

    // returning HTTP client using user's token and configs of the application
    client := authClient(ctx, config)
    svc, err := fitness.NewService(ctx, option.WithHTTPClient(client))
    if err != nil {
        log.Println("NewService error", err.Error())
        return nil, err
    }
   ...
}
Вход в полноэкранный режим Выход из полноэкранного режима

После того как мы создали авторизованного HTTP-клиента, давайте создадим запрос для агрегированного набора данных.

func GetDatasetBody(ctx context.Context, startTime, endTime time.Time, dataType string) (*fitness.AggregateResponse, error) {
    ...
    // in AggregateRequest we use milliseconds for StartTimeMillis and EndTimeMillis,
    // while in response we get time in nanoseconds
    payload := fitness.AggregateRequest{
        AggregateBy: []*fitness.AggregateBy{
            {DataTypeName: "com.google." + dataType},
        },
        BucketByTime: &fitness.BucketByTime{
            Period: &fitness.BucketByTimePeriod{
                Type:       "day",
                Value:      1,
                TimeZoneId: "GMT",
            },
        },
        StartTimeMillis: TimeToMillis(startTime),
        EndTimeMillis:   TimeToMillis(endTime),
    }

    weightData, err := svc.Users.Dataset.Aggregate("me", &payload).Do()
    if err != nil {
        return nil, errors.New("Unable to query aggregated weight data:" + err.Error())
    }

    return weightData, nil
}
Войти в полноэкранный режим Выход из полноэкранного режима

Существуют некоторые параметры, по которым можно агрегировать данные. Например, агрегация по периоду, это может быть «день», «неделя», «месяц». В нашем примере нам нужны ежедневные данные. StartTime и EndTime должны быть отформатированы в миллисекунды. Используя только одну функцию из пакета fitness Go, мы просто получаем агрегированный результат.

Результат хранится в структуре fitness.AggregateResponse. На первый взгляд ответ немного запутанный и не совсем читабельный:

Возникает много вопросов. Почему у нас 3 значения? Что это за время? Это наносекунды? Миллисекунды?

Давайте обратимся к официальной документации: https://developers.google.com/fit/datatypes/aggregate

Таким образом, мы видим, что эти 3 значения являются средним, максимальным и минимальным значениями в течение одного дня. Для примера я сделал 2 записи в приложении Google Fit: 59 кг и 60 кг, поэтому есть минимальное и максимальное значения. А 59,5 кг — это среднее значение моего веса за этот день. В базе данных Google Fit может быть много значений в течение одного дня, но в агрегированном наборе данных веса мы всегда получаем три значения.

Время хранится в наносекундах. Мы форматируем его с помощью функции NanosToTime в init.go.

Чтобы получить хороший обзор данных, давайте разберем эту структуру.

func ParseData(ds *fitness.AggregateResponse, dataType string) []models.Measurement {
    var data []models.Measurement

    for _, res := range ds.Bucket {
        for _, ds := range res.Dataset {
            for _, p := range ds.Point {
                var row models.Measurement
                row.AvValue = p.Value[0].FpVal
                row.MinValue = p.Value[1].FpVal
                row.MaxValue = p.Value[2].FpVal
                row.StartTime = NanosToTime(p.StartTimeNanos)
                row.EndTime = NanosToTime(p.EndTimeNanos)
                row.Type = dataType
                data = append(data, row)
            }
        }
    }
    return data
}
Вход в полноэкранный режим Выход из полноэкранного режима

После разбора наши данные будут выглядеть следующим образом:

Конечно, мы можем округлять значения, это зависит от вас.

Теперь давайте рассмотрим другой, более настраиваемый способ получения данных.

Индивидуальный запрос

В нашем примере мы хотим получить данные о гидратации.

В Google Fitness есть источники данных (https://developers.google.com/fit/rest/v1/data-sources), которые описывают каждый источник сенсорных данных. Источники данных могут быть разными: приложение в вашем телефоне, куда вы вручную вставляете записи, приложения или устройства, которые автоматически вставляют записи. Источники данных разделяются не только по источнику, но и по типу данных. Поэтому если нам нужны данные из какого-то определенного источника данных, который синхронизирован с Google Fitness, мы сможем получить их без проблем.

func NotAggregatedDatasets(svc *fitness.Service, minTime, maxTime time.Time, dataType string) ([]*fitness.Dataset, error) {
    ds, err := svc.Users.DataSources.List("me").DataTypeName("com.google." + dataType).Do()
    if err != nil {
        log.Println("Unable to retrieve user's data sources:", err)
        return nil, err
    }

    ...
}
Вход в полноэкранный режим Выход из полноэкранного режима

Здесь dataType — «гидратация». Подробнее о типах данных в Google Fit Питание см. здесь: https://developers.google.com/fit/datatypes/nutrition.

В каждом источнике данных вы можете найти список наборов данных.

func NotAggregatedDatasets(svc *fitness.Service, minTime, maxTime time.Time, dataType string) ([]*fitness.Dataset, error) {
    ds, err := svc.Users.DataSources.List("me").DataTypeName("com.google." + dataType).Do()
    if err != nil {
        log.Println("Unable to retrieve user's data sources:", err)
        return nil, err
    }
    if len(ds.DataSource) == 0 {
        log.Println("You have no data sources to explore.")
        return nil, err
    }

    var dataset []*fitness.Dataset

    for _, d := range ds.DataSource {
        setID := fmt.Sprintf("%v-%v", minTime.UnixNano(), maxTime.UnixNano())
        data, err := svc.Users.DataSources.Datasets.Get("me", d.DataStreamId, setID).Do()
        if err != nil {
            log.Println("Unable to retrieve dataset:", err.Error())
            return nil, err
        }
        dataset = append(dataset, data)
    }

    return dataset, nil

}
Войти в полноэкранный режим Выйти из полноэкранного режима

Полученные данные выглядят следующим образом:

Опять данные не совсем читабельны. Давайте разберем их.

func ParseHydration(datasets []*fitness.Dataset) []models.HydrationStruct {
    var data []models.HydrationStruct

    for _, ds := range datasets {
        var value float64
        for _, p := range ds.Point {
            for _, v := range p.Value {
                valueString := fmt.Sprintf("%.3f", v.FpVal)
                value, _ = strconv.ParseFloat(valueString, 64)
            }
            var row models.HydrationStruct
            row.StartTime = NanosToTime(p.StartTimeNanos)
            row.EndTime = NanosToTime(p.EndTimeNanos)
            // liters to milliliters
            row.Amount = int(value * 1000)
            data = append(data, row)
        }
    }
    return data
}
Войти в полноэкранный режим Выйти из полноэкранного режима

После этого данные будут выглядеть намного лучше!

Заключение

Ура! Теперь вы можете запросить Google Fit для получения данных о состоянии здоровья. Здесь я показал только два примера. Для получения дополнительной информации смотрите мой репозиторий, где вы найдете, как получить данные о питании, росте, шагах, сердечных точках, пульсе, активных минутах, сожженных калориях и сегментах активности.

bronnika / devto-google-fit

devto-google-fit

Посмотреть на GitHub

Я буду стараться комментировать код, чтобы лучше понять его, и надеюсь, что вам понравилось читать мою первую статью в жизни!

Следующая статья

Оставайтесь с нами до выхода второй части, где я расскажу, как записывать данные в хитрый Google Fit, чтобы ваше приложение было полностью синхронизировано!

Если у вас есть еще вопросы или предложения, я буду очень рад вас выслушать!

Оцените статью
devanswers.ru
Добавить комментарий