Google Fitにデータ移行

どうも、ゆるめのミニマリスト&へっぽこSEのアシアです。
今日の話は…

avatar

アシア

きっかけはこちら。

過去データも移行出来るツールが見当たらなかった。
諦めて、Visual Studioを私用PCにインストールした。

まずはこちらを参考に、Google Fitから歩数が取得できることを確認。https://qiita.com/tsumasakky/items/39853ee3680b1ce227e5

そして更に参考元のこちらを参考に、体重も取得できることを確認。
https://keestalkstech.com/2016/07/getting-your-weight-from-google-fit-with-c/

実は体重取得ではちょっと詰まった。
Google API Consoleで、OAuth 同意画面設定でFitness API系のスコープを全部追加した。
さらにDebugフォルダにあるtoken.jsonを削除して、改めて認証を通したら取得できた。

移行データを取得

Accupedoから歩数を取得

Accupedoのメニューから「CSVファイル送信」を選んで、Googleドライブに保存。
PCでダウンロードすると、HTMLファイルでした。なんでやねん。
見ると、年、月、日、歩数、距離(km)、消費カロリー、歩いた時間(H:MM)の順に並んでいるもよう。

からだログから体重を取得

いろんなアプリを経て、最近はからだログにデータを溜め込んでた。
これもメニューからCSVでエクスポートできたので、Googleドライブ経由でダウンロード。
こちらはヘッダもついて非常にわかりやすく出力してくれる。
実に2,691日分の記録がとれた。
とはいえ、ヘッダは邪魔なので削除しておく。

データを移行アプリについて

.NET Frameworkのデスクトップアプリとして作成。
確認用に選択日から1週間分を取得できる機能ももたせて、登録処理を実装していく。

登録処理については、こちらを参考にさせていただいた。
https://stackoverflow.com/questions/48830259/error-while-inserting-weight-data-through-c-sharp-google-fit-api

権限系のエラーが出た場合には、token.jsonを削除してやり直したらOK。

どうも、180データずつしか登録できないようだ。
なので、歯抜けがないようにファイルを分割して登録していく。

また、登録しても、取得できるようになるまで、数秒のラグがあるもよう。
スマホから見えるデータは、更にもう少しラグがありそう。
スマホから見えるデータが歯抜けになってしまった場合は、Google Fitアプリを再インストールすれば良い。

とりあえず動いて、データの移行ができた。

ソースコード

共通部

FitnessQuery

using Google.Apis.Fitness.v1;
using Google.Apis.Fitness.v1.Data;
using System;

namespace DataSyncToGoogleFit
{
    internal class FitnessQuery
    {
        protected FitnessService _service;
        private string _dataSourceId;
        private string _dataType;

        public FitnessQuery(FitnessService service, string dataSourceId, string dataType)
        {
            _service = service;
            _dataSourceId = dataSourceId;
            _dataType = dataType;
        }

        protected AggregateRequest CreateRequest(DateTime start, DateTime end, TimeSpan? bucketDuration = null)
        {
            var bucketTimeSpan = bucketDuration.GetValueOrDefault(TimeSpan.FromDays(1));
            return new AggregateRequest
            {
                AggregateBy = new AggregateBy[] { new AggregateBy { DataSourceId = _dataSourceId, DataTypeName = _dataType } },
                BucketByTime = new BucketByTime { DurationMillis = (long)bucketTimeSpan.TotalMilliseconds },
                StartTimeMillis = GoogleTime.FromDateTime(start).TotalMilliseconds,
                EndTimeMillis = GoogleTime.FromDateTime(end).TotalMilliseconds
            };
        }

        protected virtual AggregateResponse ExecuteRequest(AggregateRequest request, string userId = "me")
        {
            var agg = _service.Users.Dataset.Aggregate(request, userId);
            return agg.Execute();
        }
    }
}

GoogleTime

using System;

namespace DataSyncToGoogleFit
{
    internal class GoogleTime
    {
        private static readonly DateTime ZERO = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
        public long TotalMilliseconds { get; private set; }
        public long TotalNanoSeconds { get { return TotalMilliseconds * 1000000; } }

        private GoogleTime() { }

        public static GoogleTime FromDateTime(DateTime dt)
        {
            return new GoogleTime { TotalMilliseconds = (long)(dt - ZERO).TotalMilliseconds, };
        }

        public static GoogleTime FromNanoseconds(long? nanoseconds)
        {
            return new GoogleTime { TotalMilliseconds = (long)(nanoseconds.GetValueOrDefault(0) / 1000000) };
        }

        public DateTime ToDateTime()
        {
            return ZERO.AddMilliseconds(this.TotalMilliseconds);
        }
    }
}

歩数情報を管理する部分

ReadStepQuery

using Google.Apis.Fitness.v1;
using System;
using System.Collections.Generic;
using System.Linq;

namespace DataSyncToGoogleFit
{
    internal class ReadStepQuery : FitnessQuery
    {
        internal class StepDataPoint
        {
            public int? Step { get; set; }
            public DateTime Stamp { get; set; }
        }

        public ReadStepQuery(FitnessService service)
            : base(service, "derived:com.google.step_count.delta:com.google.android.gms:merge_step_deltas", "com.google.step_count.delta")
        {
        }

        public IList<ReadStepQuery.StepDataPoint> CreateQuery(DateTime start, DateTime end)
        {
            var request = CreateRequest(start, end);
            var response = ExecuteRequest(request);

            return response
              .Bucket
              .SelectMany(b => b.Dataset)
              .Where(d => d.Point != null)
              .SelectMany(d => d.Point)
              .Where(p => p.Value != null)
              .SelectMany(p =>
              {
                  return p.Value.Select(v =>
                    new StepDataPoint
                    {
                        Step = v.IntVal.GetValueOrDefault(),
                        Stamp = GoogleTime.FromNanoseconds(p.StartTimeNanos).ToDateTime()
                    });
              }).ToList();
        }
    }
}

WriteStepQuery

歩数情報は、StartTimeNanosとEndTimeNanosを同一にしてはいけないっぽい。
ので、00:00:00~23:59:59範囲を指定した。

using Google.Apis.Fitness.v1.Data;
using Google.Apis.Fitness.v1;
using System;
using System.Collections.Generic;

namespace DataSyncToGoogleFit
{
    internal class WriteStepQuery : FitnessQuery
    {
        public WriteStepQuery(FitnessService service)
            : base(service, "raw:com.google.step_count.cumulativ:com.google.android.apps.fitness:user_input", "com.google.step_count.delta")
        {
        }

        public void CreateQuery(List<KeyValuePair<DateTime, int>> measures, string clientId)
        {
            DataSource dataSource = new DataSource()
            {
                Type = "derived",
                Application = new Application() { Name = "estimated_steps" },
                DataType = new DataType()
                {
                    Name = "com.google.step_count.delta",
                    Field = new List<DataTypeField>() { new DataTypeField() { Name = "steps", Format = "integer" } }
                },
                Device = new Device() { Type = "tablet", Manufacturer = "unknown", Model = "unknown", Uid = "unknown", Version = "1.0" }
            };

            string dataSourceId = $"{dataSource.Type}:{dataSource.DataType.Name}:{clientId.Split('-')[0]}:{dataSource.Device.Manufacturer}:{dataSource.Device.Model}:{dataSource.Device.Uid}";
            try
            {
                DataSource googleDataSource = _service.Users.DataSources.Get("me", dataSourceId).Execute();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
                DataSource googleDataSource = _service.Users.DataSources.Create(dataSource, "me").Execute();
            }

            Dataset stepDataSource = new Dataset()
            {
                DataSourceId = dataSourceId,
                Point = new List<DataPoint>()
            };

            DateTime minDateTime = DateTime.MaxValue;
            DateTime maxDateTime = DateTime.MinValue;
            foreach (var step in measures)
            {
                GoogleTime ts = GoogleTime.FromDateTime(step.Key);
                GoogleTime end = GoogleTime.FromDateTime(step.Key.AddHours(23).AddMinutes(59).AddSeconds(59));
                stepDataSource.Point.Add
                (
                    new DataPoint()
                    {
                        DataTypeName = "com.google.step_count.delta",
                        StartTimeNanos = ts.TotalNanoSeconds,
                        EndTimeNanos = end.TotalNanoSeconds,
                        Value = new List<Value>() { new Value() { IntVal = step.Value } }
                    }
                );

                if (minDateTime > step.Key) minDateTime = step.Key;
                if (maxDateTime < step.Key) maxDateTime = step.Key;
            }

            stepDataSource.MinStartTimeNs = GoogleTime.FromDateTime(minDateTime).TotalNanoSeconds;
            stepDataSource.MaxEndTimeNs = GoogleTime.FromDateTime(maxDateTime.AddHours(23).AddMinutes(59).AddSeconds(59)).TotalNanoSeconds;
            string dataSetId = stepDataSource.MinStartTimeNs.ToString() + "-" + stepDataSource.MaxEndTimeNs.ToString();
            var save = _service.Users.DataSources.Datasets.Patch(stepDataSource, "me", dataSourceId, dataSetId).Execute();
        }
    }
}

体重情報を管理する部分

ReadWeightQuery

using Google.Apis.Fitness.v1;
using System;
using System.Collections.Generic;
using System.Linq;

namespace DataSyncToGoogleFit
{
    internal class ReadWeightQuery : FitnessQuery
    {
        internal class WeightDataPoint
        {
            public double? Weight { get; set; }
            public DateTime Stamp { get; set; }
        }
        public ReadWeightQuery(FitnessService service)
            : base(service, "derived:com.google.weight:com.google.android.gms:merge_weight", "com.google.weight.summary")
        {
        }

        public IList<ReadWeightQuery.WeightDataPoint> CreateQuery(DateTime start, DateTime end)
        {
            var request = CreateRequest(start, end);
            var response = ExecuteRequest(request);

            return response
                .Bucket
                .SelectMany(b => b.Dataset)
                .Where(d => d.Point != null)
                .SelectMany(d => d.Point)
                .Where(p => p.Value != null)
                .SelectMany(p =>
                {
                    return p.Value.Select(v =>
                        new WeightDataPoint
                        {
                            Weight = v.FpVal.GetValueOrDefault(),
                            Stamp = GoogleTime.FromNanoseconds(p.StartTimeNanos).ToDateTime()
                        });
                }).ToList();
        }
    }
}

WriteWeightQuery

using Google.Apis.Fitness.v1;
using Google.Apis.Fitness.v1.Data;
using System;
using System.Collections.Generic;

namespace DataSyncToGoogleFit
{
    internal class WriteWeightQuery : FitnessQuery
    {
        public WriteWeightQuery(FitnessService service)
            : base(service, "raw:com.google.weight:com.google.android.apps.fitness:user_input", "com.google.weight.summary")
        {
        }

        public void CreateQuery(List<KeyValuePair<DateTime, float>> measures, string clientId)
        {
            DataSource dataSource = new DataSource()
            {
                Type = "raw",
                Application = new Application() { Name = "maweightimport" },
                DataType = new DataType()
                {
                    Name = "com.google.weight",
                    Field = new List<DataTypeField>() { new DataTypeField() { Name = "weight", Format = "floatPoint" } }
                },
                Device = new Device() { Type = "scale", Manufacturer = "unknown", Model = "unknown", Uid = "maweightimport", Version = "1.0" }
            };

            string dataSourceId = $"{dataSource.Type}:{dataSource.DataType.Name}:{clientId.Split('-')[0]}:{dataSource.Device.Manufacturer}:{dataSource.Device.Model}:{dataSource.Device.Uid}";
            try
            {
                DataSource googleDataSource = _service.Users.DataSources.Get("me", dataSourceId).Execute();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
                DataSource googleDataSource = _service.Users.DataSources.Create(dataSource, "me").Execute();
            }

            Dataset weightsDataSource = new Dataset()
            {
                DataSourceId = dataSourceId,
                Point = new List<DataPoint>()
            };

            DateTime minDateTime = DateTime.MaxValue;
            DateTime maxDateTime = DateTime.MinValue;
            foreach (var weight in measures)
            {
                GoogleTime ts = GoogleTime.FromDateTime(weight.Key);
                weightsDataSource.Point.Add
                (
                    new DataPoint()
                    {
                        DataTypeName = "com.google.weight",
                        StartTimeNanos = ts.TotalNanoSeconds,
                        EndTimeNanos = ts.TotalNanoSeconds,
                        Value = new List<Value>() { new Value() { FpVal = weight.Value } }
                    }
                );

                if (minDateTime > weight.Key) minDateTime = weight.Key;
                if (maxDateTime < weight.Key) maxDateTime = weight.Key;
            }

            weightsDataSource.MinStartTimeNs = GoogleTime.FromDateTime(minDateTime).TotalNanoSeconds;
            weightsDataSource.MaxEndTimeNs = GoogleTime.FromDateTime(maxDateTime).TotalNanoSeconds;
            string dataSetId = weightsDataSource.MinStartTimeNs.ToString() + "-" + weightsDataSource.MaxEndTimeNs.ToString();
            var save = _service.Users.DataSources.Datasets.Patch(weightsDataSource, "me", dataSourceId, dataSetId).Execute();
        }
    }
}

Form部分

using System;
using System.Threading.Tasks;
using System.Windows.Forms;
using Google.Apis.Fitness.v1;
using System.IO;
using System.Threading;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Services;
using Google.Apis.Util.Store;
using System.Collections.Generic;

namespace DataSyncToGoogleFit
{
    public partial class Form1 : Form
    {
        private const string CLIENT_ID = "xxxxxxxxxx";
        private const string JSON_FILE_PATH = @"dokokaniaru\client_secret_xxxxxxxxxx.apps.googleusercontent.com.json";
        
        private const string WEIGHT_FILE_PATH = @"dokokaniaru\からだログ データ.eml";
        private const string STEP_FILE_PATH = @"dokokaniaru\Accupedo毎日記録.html";

        public Form1()
        {
            InitializeComponent();
        }

        /// <summary>
        /// 歩数取得ボタンクリック
        /// </summary>
        private async void BtnGetStep_Click(object sender, EventArgs e)
        {
            ICredential credential = await GetUserCredential();
            var service = new FitnessService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = "Get Fitness Step"
            });

            var step = new ReadStepQuery(service);
            DateTime starttime = Calendar.SelectionStart;
            DateTime endtime = starttime.AddDays(7);
            var results = step.CreateQuery(starttime, endtime);
            TxtboxResult.Text = "*-*-*-*-*-*-* 歩数取得結果 *-*-*-*-*-*-*\r\n";
            foreach (var result in results)
            {
                TxtboxResult.Text += result.Stamp.ToString() + " = " + result.Step + "\r\n";
            }
        }

        /// <summary>
        /// OAuth認証を用いてCredentialを取得する。
        /// </summary>
        private Task<UserCredential> GetUserCredential()
        {
            // 歩数と体重のRead/Write
            var scopes = new[] {
                FitnessService.Scope.FitnessActivityRead,
                FitnessService.Scope.FitnessActivityWrite,
                FitnessService.Scope.FitnessBodyRead,
                FitnessService.Scope.FitnessBodyWrite,
            };

            using (var stream = new FileStream(JSON_FILE_PATH, FileMode.Open, FileAccess.Read))
            {
                string credPath = "token.json";
                return GoogleWebAuthorizationBroker.AuthorizeAsync(
                  GoogleClientSecrets.Load(stream).Secrets,
                  scopes,
                  "user",
                  CancellationToken.None,
                  new FileDataStore(credPath, true));
            }
        }

        /// <summary>
        /// 体重取得ボタン
        /// </summary>
        private async void BtnGetWeight_Click(object sender, EventArgs e)
        {
            ICredential credential = await GetUserCredential();
            var service = new FitnessService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = "Get Fitness Weight"
            });

            var query = new ReadWeightQuery(service);
            DateTime starttime = Calendar.SelectionStart;
            DateTime endtime = starttime.AddDays(7);
            var results = query.CreateQuery(starttime, endtime);
            TxtboxResult.Text = "*-*-*-*-*-*-* 体重取得結果 *-*-*-*-*-*-*\r\n";
            foreach (var result in results)
            {
                TxtboxResult.Text += result.Stamp.ToString() + " = " + result.Weight + "\r\n";
            }
        }

        /// <summary>
        /// 体重書き込みボタン
        /// </summary>
        private async void BtnSetWeight_Click(object sender, EventArgs e)
        {
            ICredential credential = await GetUserCredential();
            var service = new FitnessService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = "Write Fitness Weight"
            });
            var query = new WriteWeightQuery(service);

            // ファイルから記録するデータを取得
            List<KeyValuePair<DateTime, float>> measures = new List<KeyValuePair<DateTime, float>>();
            string[] lines = File.ReadAllLines(WEIGHT_FILE_PATH);
            for(int i = 0;i < lines.Length; i++)
            {
                string[] item = lines[i].Split(',');
                float.TryParse(item[3], out float floatval);
                // 記録がない場合は除外
                if (floatval == 0) continue;
                string[] dateitem = item[1].Split('/');
                DateTime date = new DateTime(int.Parse(dateitem[0]), int.Parse(dateitem[1]), int.Parse(dateitem[2]));
                measures.Add(new KeyValuePair<DateTime, float>(date, floatval));
            }
            query.CreateQuery(measures, CLIENT_ID);
        }

        /// <summary>
        /// 歩数登録ボタン
        /// </summary>
        private async void BtnSetStep_Click(object sender, EventArgs e)
        {
            ICredential credential = await GetUserCredential();
            var service = new FitnessService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = "Write Fitness Step"
            });
            var query = new WriteStepQuery(service);
            List<KeyValuePair<DateTime, int>> measures = new List<KeyValuePair<DateTime, int>>();
            string[] lines = File.ReadAllLines(STEP_FILE_PATH);
            for (int i = 0; i < lines.Length; i++)
            {
                string[] item = lines[i].Split(',');
                int.TryParse(item[3], out int intval);
                DateTime date = new DateTime(int.Parse(item[0].Trim()), int.Parse(item[1].Trim()), int.Parse(item[2].Trim()));
                measures.Add(new KeyValuePair<DateTime, int>(date, intval));
            }
            query.CreateQuery(measures, CLIENT_ID);
        }
    }
}

朝飯・昼食を忘れて、続きには手を出さないと誓ったにも関わらず手を出した。
おかげで、1日でできたけど、なんだろう、この敗北感。


他にもニッチなIT関連要素をまとめていますので、よければ一覧記事もご覧ください。

返信を残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)