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