Skip to content

Commit 80aa755

Browse files
committed
Add support picooc scales
1 parent ef8c33f commit 80aa755

5 files changed

Lines changed: 290 additions & 2 deletions

File tree

README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ Getting data from all scales of the selected user:
160160
161161
```yaml
162162
sync_zepp:
163-
from: zepp/xiaomi {username} {password} {username}
163+
from: zepp/xiaomi {username} {password} {user}
164164
to: csv alex_zepp.csv
165165
```
166166
@@ -174,6 +174,24 @@ sync_tanita:
174174
to: csv alex_tanita.csv
175175
```
176176
177+
### Picooc
178+
179+
Get data from all scales of the main user:
180+
181+
```yaml
182+
sync_picooc:
183+
from: picooc {username} {password}
184+
to: csv alex_picooc.csv
185+
```
186+
187+
Getting data from all scales of the selected user:
188+
189+
```yaml
190+
sync_picooc:
191+
from: picooc {username} {password} {user}
192+
to: csv alex_picooc.csv
193+
```
194+
177195
### Fitbit
178196
179197
My Fitbit Aria scales are no longer working. Therefore, I am interested in this service only from the point of view of archived data.

internal/accounts.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/AlexxIT/SmartScaleConnect/pkg/core"
77
"github.com/AlexxIT/SmartScaleConnect/pkg/garmin"
8+
"github.com/AlexxIT/SmartScaleConnect/pkg/picooc"
89
"github.com/AlexxIT/SmartScaleConnect/pkg/tanita"
910
"github.com/AlexxIT/SmartScaleConnect/pkg/xiaomi"
1011
"github.com/AlexxIT/SmartScaleConnect/pkg/zepp"
@@ -34,6 +35,8 @@ func getAccount(fields []string, key string) (core.Account, error) {
3435
switch fields[0] {
3536
case "garmin":
3637
acc = garmin.NewClient()
38+
case "picooc":
39+
acc = picooc.NewClient()
3740
case "tanita":
3841
acc = tanita.NewClient()
3942
case "xiaomi":

internal/weights.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func getWeights(config string) ([]*core.Weight, error) {
7070
}
7171
return acc.GetAllWeights()
7272

73-
case "xiaomi", "zepp/xiaomi":
73+
case "picooc", "xiaomi", "zepp/xiaomi":
7474
acc, err := GetAccount(fields)
7575
if err != nil {
7676
return nil, err

pkg/picooc/auth.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package picooc
2+
3+
import (
4+
"crypto/md5"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"net/url"
9+
"strconv"
10+
"strings"
11+
"time"
12+
13+
"github.com/google/uuid"
14+
)
15+
16+
const api = "https://api2.picooc-int.com/v1/api/"
17+
18+
func (c *Client) Login(username, password string) error {
19+
form := c.values("user_login_new")
20+
21+
var req1 struct {
22+
AppVer string `json:"appver"`
23+
Timestamp string `json:"timestamp"`
24+
Lang string `json:"lang"`
25+
Method string `json:"method"`
26+
Timezone string `json:"timezone"`
27+
Sign string `json:"sign"`
28+
PushToken string `json:"push_token"`
29+
DeviceID string `json:"device_id"`
30+
Rec struct {
31+
AppChannel string `json:"app_channel"`
32+
AppVer string `json:"app_version"`
33+
Email string `json:"email"`
34+
Password string `json:"password"`
35+
Phone string `json:"phone"`
36+
PhoneSystem string `json:"phone_system"`
37+
PhoneType string `json:"phone_type"`
38+
} `json:"req"`
39+
}
40+
req1.AppVer = form.Get("appver")
41+
req1.Timestamp = form.Get("timestamp")
42+
req1.Lang = form.Get("lang")
43+
req1.Method = form.Get("method")
44+
req1.Sign = form.Get("sign")
45+
req1.PushToken = form.Get("push_token")
46+
req1.DeviceID = form.Get("device_id")
47+
req1.Rec.AppVer = form.Get("appver")
48+
req1.Rec.Email = username
49+
req1.Rec.Password = password
50+
51+
data, err := json.Marshal(req1)
52+
if err != nil {
53+
return err
54+
}
55+
56+
form.Set("reqData", string(data))
57+
58+
res, err := c.client.Post(
59+
api+"account/login", "application/x-www-form-urlencoded", strings.NewReader(form.Encode()),
60+
)
61+
if err != nil {
62+
return err
63+
}
64+
defer res.Body.Close()
65+
66+
var res1 struct {
67+
Code int `json:"code"`
68+
Msg string `json:"msg"`
69+
Resp struct {
70+
UserID string `json:"user_id"`
71+
RoleID string `json:"role_id"`
72+
Roles []struct {
73+
RoleID string `json:"role_id"`
74+
RoleName string `json:"role_name"`
75+
} `json:"roles"`
76+
//EndBodyIndex json.RawMessage `json:"end_body_index"`
77+
} `json:"resp"`
78+
}
79+
80+
if err = json.NewDecoder(res.Body).Decode(&res1); err != nil {
81+
return err
82+
}
83+
84+
if res1.Code != 0 {
85+
return errors.New("picooc: login error: " + res1.Msg)
86+
}
87+
88+
c.userID = res1.Resp.UserID
89+
90+
c.roleIDs = map[string]string{"": res1.Resp.RoleID}
91+
for _, role := range res1.Resp.Roles {
92+
c.roleIDs[role.RoleName] = role.RoleID
93+
}
94+
95+
return nil
96+
}
97+
98+
const appVer = "i4.1.11.0"
99+
100+
func (c *Client) values(method string) url.Values {
101+
if c.deviceID == "" {
102+
c.deviceID = strings.ToUpper(uuid.NewString())
103+
}
104+
105+
timestamp := strconv.Itoa(int(time.Now().Unix()))
106+
sign := upperMD5(c.deviceID + upperMD5(timestamp+upperMD5(method)+upperMD5(appVer)))
107+
108+
return url.Values{
109+
"appver": {appVer},
110+
"timestamp": {timestamp},
111+
"lang": {"en"},
112+
"method": {method},
113+
"timezone": {""}, // don't know how to get right value
114+
"sign": {sign},
115+
"push_token": {"android::" + c.deviceID},
116+
"device_id": {c.deviceID},
117+
}
118+
}
119+
120+
func upperMD5(s string) string {
121+
return fmt.Sprintf("%X", md5.Sum([]byte(s)))
122+
}

pkg/picooc/client.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package picooc
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"net/http"
7+
"strconv"
8+
"time"
9+
10+
"github.com/AlexxIT/SmartScaleConnect/pkg/core"
11+
)
12+
13+
type Client struct {
14+
client *http.Client
15+
16+
deviceID string
17+
userID string
18+
19+
roleIDs map[string]string
20+
}
21+
22+
func NewClient() *Client {
23+
return &Client{
24+
client: &http.Client{Timeout: time.Minute},
25+
}
26+
}
27+
28+
func (c *Client) GetAllWeights() ([]*core.Weight, error) {
29+
return c.GetFilterWeights("")
30+
}
31+
32+
func (c *Client) GetFilterWeights(name string) ([]*core.Weight, error) {
33+
roleID, ok := c.roleIDs[name]
34+
if !ok {
35+
return nil, errors.New("picooc: unknown user: " + name)
36+
}
37+
38+
var weights []*core.Weight
39+
40+
params := c.values("bodyIndexList")
41+
//params.Set("orderType", "-1")
42+
params.Set("pageSize", "1000")
43+
params.Set("time", params.Get("timestamp"))
44+
params.Set("userId", c.userID)
45+
params.Set("roleId", roleID)
46+
47+
for {
48+
res, err := c.client.Get(api + "bodyIndex/bodyIndexList?" + params.Encode())
49+
if err != nil {
50+
return nil, err
51+
}
52+
defer res.Body.Close()
53+
54+
var res1 struct {
55+
//Code int `json:"code"`
56+
//Msg string `json:"msg"`
57+
//Method string `json:"method"`
58+
//Result struct {
59+
// Code int `json:"code"`
60+
// Message string `json:"message"`
61+
//} `json:"result"`
62+
Resp struct {
63+
Records []struct {
64+
BodyTime int64 `json:"bodyTime"`
65+
DataType int `json:"dataType"`
66+
BodyIndexId int `json:"body_index_id"`
67+
RoleId int `json:"role_id"`
68+
BodyFat float32 `json:"body_fat"`
69+
Weight float32 `json:"weight"`
70+
BMI float32 `json:"bmi"`
71+
VisceralFatLevel int `json:"visceral_fat_level"`
72+
MuscleRace float32 `json:"muscle_race"`
73+
BodyAge int `json:"body_age"`
74+
BoneMass float32 `json:"bone_mass"`
75+
BasicMetabolism int `json:"basic_metabolism"`
76+
WaterRace float32 `json:"water_race"`
77+
SkeletalMuscle float32 `json:"skeletal_muscle"`
78+
LocalTime int `json:"local_time"`
79+
SubcutaneousFat float32 `json:"subcutaneous_fat"`
80+
ServerTime int `json:"server_time"`
81+
ServerId int `json:"server_id"`
82+
IsDel int `json:"is_del"`
83+
Abnormal struct {
84+
Weight int `json:"weight"`
85+
Time int `json:"time"`
86+
AbnormalFlag int `json:"abnormal_flag"`
87+
BodyFat int `json:"body_fat"`
88+
} `json:"abnormal"`
89+
AbnormalFlag int `json:"abnormal_flag"`
90+
ElectricResistance int `json:"electric_resistance"`
91+
IsManuallyAdd int `json:"is_manually_add"`
92+
IsFirstDay int `json:"is_first_day"`
93+
LandmarkIcons []string `json:"landmarkIcons"`
94+
MAC string `json:"mac"`
95+
AnchorWeight int `json:"anchor_weight"`
96+
AnchorBata int `json:"anchor_bata"`
97+
CorrectionValueR int `json:"correction_value_r"`
98+
BodyFatReferenceValue float32 `json:"body_fat_reference_value"`
99+
LabelMarker int `json:"label_marker"`
100+
DataSources int `json:"data_sources"`
101+
} `json:"records"`
102+
LastTime int `json:"lastTime"`
103+
Continue bool `json:"continue"`
104+
} `json:"resp"`
105+
}
106+
107+
if err = json.NewDecoder(res.Body).Decode(&res1); err != nil {
108+
return nil, err
109+
}
110+
111+
for _, v1 := range res1.Resp.Records {
112+
if v1.AbnormalFlag != 0 || v1.IsDel != 0 {
113+
continue
114+
}
115+
116+
w := &core.Weight{
117+
Date: time.Unix(v1.BodyTime, 0),
118+
Weight: v1.Weight,
119+
120+
BMI: v1.BMI,
121+
BodyFat: v1.BodyFat,
122+
BodyWater: v1.WaterRace,
123+
BoneMass: v1.BoneMass,
124+
125+
MetabolicAge: v1.BodyAge, // 0
126+
VisceralFat: v1.VisceralFatLevel,
127+
128+
BasalMetabolism: v1.BasicMetabolism,
129+
SkeletalMuscleMass: v1.SkeletalMuscle, // 0
130+
131+
User: name,
132+
Source: v1.MAC,
133+
}
134+
weights = append(weights, w)
135+
}
136+
137+
if !res1.Resp.Continue {
138+
break
139+
}
140+
141+
params.Set("time", strconv.Itoa(res1.Resp.LastTime))
142+
}
143+
144+
return weights, nil
145+
}

0 commit comments

Comments
 (0)