From: Florian Forster Date: Wed, 31 Jan 2018 15:44:31 +0000 (+0100) Subject: Package fitbit: Implement Sleep() to read sleep logs. X-Git-Url: https://git.octo.it/?p=kraftakt.git;a=commitdiff_plain;h=0760c07303a8b8a78a4858e7331d3189f3d83af3 Package fitbit: Implement Sleep() to read sleep logs. --- diff --git a/fitbit/sleep.go b/fitbit/sleep.go new file mode 100644 index 0000000..76fc197 --- /dev/null +++ b/fitbit/sleep.go @@ -0,0 +1,122 @@ +package fitbit + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "time" + + "google.golang.org/appengine/log" +) + +// SleepLevel is the depth of sleep. +// +// The Fitbit API provides these in one of two ways. The newer representation +// is ("deep", "light", "rem" and "wake"), the previous (older) representation +// is ("asleep", "restless" and "awake"). We map the old representation to the +// newer one, because we'd need to do that for Google Fit anyway. This is done +// like so: +// +// asleep → deep +// restless → light +// awake → wake +type SleepLevel int + +const ( + SleepLevelUnknown SleepLevel = iota + SleepLevelDeep + SleepLevelLight + SleepLevelREM + SleepLevelWake +) + +// SleepStage is one stage during a user's sleep. +type SleepStage struct { + StartTime time.Time + EndTime time.Time + Level SleepLevel +} + +// Sleep is one period of sleep that is broken down into multiple stages. +type Sleep struct { + Stages []SleepStage +} + +func parseSleep(ctx context.Context, data []byte, loc *time.Location) (*Sleep, error) { + type rawData struct { + DateTime string + Level string + Seconds int64 + } + var jsonSleep struct { + Levels struct { + Data []rawData + ShortData []rawData + } + } + if err := json.Unmarshal(data, &jsonSleep); err != nil { + return nil, err + } + + rawStages := jsonSleep.Levels.Data + if len(jsonSleep.Levels.ShortData) != 0 { + rawStages = jsonSleep.Levels.ShortData + } + + var ret Sleep + for _, stg := range rawStages { + tm, err := time.ParseInLocation("2006-01-02T15:04:05.999", stg.DateTime, loc) + if err != nil { + log.Warningf(ctx, "unable to parse time: %q", stg.DateTime) + continue + } + + var level SleepLevel + switch stg.Level { + case "deep", "asleep": + level = SleepLevelDeep + case "light", "restless": + level = SleepLevelLight + case "rem": + level = SleepLevelREM + case "wake", "awake": + level = SleepLevelWake + default: + log.Warningf(ctx, "unknown sleep level: %q", stg.Level) + continue + } + + ret.Stages = append(ret.Stages, SleepStage{ + StartTime: tm, + EndTime: tm.Add(time.Duration(stg.Seconds) * time.Second), + Level: level, + }) + } + + if len(ret.Stages) == 0 && len(jsonSleep.Levels.Data) != 0 { + return nil, fmt.Errorf("parsing sleep stages failed") + } + + return &ret, nil +} + +// Sleep returns the sleep log for date. Times are parsed in the user's timeozne, loc. +func (c *Client) Sleep(ctx context.Context, date string, loc *time.Location) (*Sleep, error) { + url := fmt.Sprintf("https://api.fitbit.com/1.2/user/%s/sleep/date/%s.json", + c.fitbitUserID, date) + + res, err := c.client.Get(url) + if err != nil { + return nil, err + } + defer res.Body.Close() + + data, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, err + } + log.Debugf(ctx, "GET %s -> %s", url, data) + + return parseSleep(ctx, data, loc) +} diff --git a/fitbit/sleep_test.go b/fitbit/sleep_test.go new file mode 100644 index 0000000..5d83858 --- /dev/null +++ b/fitbit/sleep_test.go @@ -0,0 +1,116 @@ +package fitbit + +import ( + "context" + "reflect" + "testing" + "time" +) + +func TestParseSleep(t *testing.T) { + ctx := context.Background() + + input := `{ + "dateOfSleep": "2017-04-02", + "duration": 42000, + "efficiency": 42, + "isMainSleep": true, + "levels": { + "summary": { + "deep": { + "count": 42, + "minutes": 42, + "thirtyDayAvgMinutes": 42 + }, + "light": { + "count": 42, + "minutes": 42, + "thirtyDayAvgMinutes": 42 + }, + "rem": { + "count": 42, + "minutes": 42, + "thirtyDayAvgMinutes": 42 + }, + "wake": { + "count": 42, + "minutes": 42, + "thirtyDayAvgMinutes": 42 + } + }, + "data": [ + { + "datetime": "2017-04-01T23:58:30.000", + "level": "wake", + "seconds": 1080 + }, + { + "datetime": "2017-04-02T00:16:30.000", + "level": "rem", + "seconds": 2000 + } + ], + "shortData": [ + { + "datetime": "2017-04-01T23:58:30.000", + "level": "wake", + "seconds": 1080 + }, + { + "datetime": "2017-04-02T00:16:30.000", + "level": "deep", + "seconds": 100 + }, + { + "datetime": "2017-04-02T00:18:10.000", + "level": "rem", + "seconds": 1900 + } + ] + }, + "logId": 42, + "minutesAfterWakeup": 42, + "minutesAsleep": 42, + "minutesAwake": 42, + "minutesToFallAsleep": 0, + "startTime": "2017-04-01T23:58:30.000", + "timeInBed": 42, + "type": "stages" +}` + + want := &Sleep{ + Stages: []SleepStage{ + SleepStage{ + StartTime: time.Date(2017, time.April, 1, 23, 58, 30, 0, time.UTC), + EndTime: time.Date(2017, time.April, 2, 0, 16, 30, 0, time.UTC), + Level: SleepLevelWake, + }, + SleepStage{ + StartTime: time.Date(2017, time.April, 2, 0, 16, 30, 0, time.UTC), + EndTime: time.Date(2017, time.April, 2, 0, 18, 10, 0, time.UTC), + Level: SleepLevelDeep, + }, + SleepStage{ + StartTime: time.Date(2017, time.April, 2, 0, 18, 10, 0, time.UTC), + EndTime: time.Date(2017, time.April, 2, 0, 49, 50, 0, time.UTC), + Level: SleepLevelREM, + }, + }, + } + + got, err := parseSleep(ctx, []byte(input), time.UTC) + if err != nil { + t.Errorf("parseSleep() = %v", err) + } + + for i, stg := range got.Stages { + t.Logf("got.Stages[%d] = %+v", i, stg) + } + for i, stg := range want.Stages { + t.Logf("want.Stages[%d] = %+v", i, stg) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("parseSleep() = %+v, want %+v", got, want) + } +}