Contents
どうも、ゆるめのミニマリスト&へっぽこSEのアシアです。
今日の話は…
アシア
きっかけはこちら。
過去データも移行出来るツールが見当たらなかった。
諦めて、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関連要素をまとめていますので、よければ一覧記事もご覧ください。