d7da1386c4a7c631e722db96e68b703aa7b2d022
[kraftakt.git] / fitbit / sleep.go
1 package fitbit
2
3 import (
4         "context"
5         "encoding/json"
6         "fmt"
7         "io/ioutil"
8         "time"
9
10         "google.golang.org/appengine/log"
11 )
12
13 // SleepLevel is the depth of sleep.
14 //
15 // The Fitbit API provides these in one of two ways. The newer representation
16 // is ("deep", "light", "rem" and "wake"), the previous (older) representation
17 // is ("asleep", "restless" and "awake"). We map the old representation to the
18 // newer one, because we'd need to do that for Google Fit anyway. This is done
19 // like so:
20 //
21 //   asleep    →  deep
22 //   restless  →  light
23 //   awake     →  wake
24 type SleepLevel int
25
26 const (
27         SleepLevelUnknown SleepLevel = iota
28         SleepLevelDeep
29         SleepLevelLight
30         SleepLevelREM
31         SleepLevelWake
32 )
33
34 // SleepStage is one stage during a user's sleep.
35 type SleepStage struct {
36         StartTime time.Time
37         EndTime   time.Time
38         Level     SleepLevel
39 }
40
41 // Sleep is one period of sleep that is broken down into multiple stages.
42 type Sleep struct {
43         Stages []SleepStage
44 }
45
46 func parseSleep(ctx context.Context, data []byte, loc *time.Location) (*Sleep, error) {
47         type rawData struct {
48                 DateTime string
49                 Level    string
50                 Seconds  int64
51         }
52         var jsonSleep struct {
53                 Levels struct {
54                         Data      []rawData
55                         ShortData []rawData
56                 }
57         }
58         if err := json.Unmarshal(data, &jsonSleep); err != nil {
59                 return nil, err
60         }
61
62         rawStages := jsonSleep.Levels.Data
63         if len(jsonSleep.Levels.ShortData) != 0 {
64                 rawStages = append(rawStages, jsonSleep.Levels.ShortData...)
65         }
66
67         var ret Sleep
68         for _, stg := range rawStages {
69                 tm, err := time.ParseInLocation("2006-01-02T15:04:05.999", stg.DateTime, loc)
70                 if err != nil {
71                         log.Warningf(ctx, "unable to parse time: %q", stg.DateTime)
72                         continue
73                 }
74
75                 var level SleepLevel
76                 switch stg.Level {
77                 case "deep", "asleep":
78                         level = SleepLevelDeep
79                 case "light", "restless":
80                         level = SleepLevelLight
81                 case "rem":
82                         level = SleepLevelREM
83                 case "wake", "awake":
84                         level = SleepLevelWake
85                 default:
86                         log.Warningf(ctx, "unknown sleep level: %q", stg.Level)
87                         continue
88                 }
89
90                 ret.Stages = append(ret.Stages, SleepStage{
91                         StartTime: tm,
92                         EndTime:   tm.Add(time.Duration(stg.Seconds) * time.Second),
93                         Level:     level,
94                 })
95         }
96
97         if len(ret.Stages) == 0 && len(jsonSleep.Levels.Data) != 0 {
98                 return nil, fmt.Errorf("parsing sleep stages failed")
99         }
100
101         return &ret, nil
102 }
103
104 // Sleep returns the sleep log for date t. Times are parsed in the same timezone as t.
105 func (c *Client) Sleep(ctx context.Context, t time.Time) (*Sleep, error) {
106         url := fmt.Sprintf("https://api.fitbit.com/1.2/user/%s/sleep/date/%s.json",
107                 c.fitbitUserID, t.Format("2006-01-02"))
108
109         res, err := c.client.Get(url)
110         if err != nil {
111                 return nil, err
112         }
113         defer res.Body.Close()
114
115         data, err := ioutil.ReadAll(res.Body)
116         if err != nil {
117                 return nil, err
118         }
119         log.Debugf(ctx, "GET %s -> %s", url, data)
120
121         return parseSleep(ctx, data, t.Location())
122 }