Lipro
2023 年 9 月 8 日
给 Lipro 写 Home Assistant 插件(逆向篇)
记录逆向Lipro API及算法备忘
第一步:请求 CODE
移动应用授权登录
-
登陆前 GET https://open-api.flyme.cn/oauth/authorize?response_type=code&scope=uc_basic_info&client_id=LABG5jq09SHTWfjOSKr5&redirect_uri=http://www.meizu.com 返回 Set-Cookie: OSESSIONID,JSESSIONID 并 302 跳转登陆页面
-
登录页面 GET https://open-api.flyme.cn/login.do?display=login&redirect_url=https://open-api.flyme.cn/oauth/authorize?response_type=code&redirect_uri=http://www.meizu.com&scope=uc_basic_info&client_id=LABG5jq09SHTWfjOSKr5 返回 Set-Cookie: loginSessionId
-
点击登录后,POST 登录表单内容到 https://open-api.flyme.cn/oauth/login.do
参数 | 说明 |
---|---|
redirect_url | 跳转认证服务器链接 |
password | 通过 excutePP (password, cryKK)函数对于 password 的每个字符,它将其与 cryKK 进行异或运算,再 URL 编码后返回。 |
cryKK | 随机生成,范围 1~1000 整数 |
ExcutePP 加密算法还原成 python 代码如下
def executePP(password, cryKK):
# 初始化一个空列表用于存储编码后的字符
encoded = []
# 遍历密码字符串中的每个字符
for character in password:
# 对字符进行 XOR 操作
encoded_char = chr(cryKK ^ ord(character))
# 如果编码后的字符是字母,则直接添加到列表中
if encoded_char.isalpha():
encoded.append(encoded_char)
else:
# 如果不是字母,则转换为十六进制表示,并添加到列表中
encoded.append(f'%{ord(encoded_char):02x}')
# 将列表中的所有元素连接成一个字符串并返回
return ''.join(encoded)
# 示例用法
password = "password"
cryKK = 40
encoded = executePP(password, cryKK)
print(encoded) # 输出: XI%5b%5b%5fGZL
返回 Cookie
参数 | 说明 |
---|---|
OSESSIONID | 登录后获得,跳转访问认证服务器时使用需要 |
JSESSIONID | 标识会话,具体用处还不清楚 |
loginSessionId | 值与 OSESSIONID 相同 |
返回示例
请求内容Request
POST https://open-api.flyme.cn/oauth/login.do HTTP/1.1
Host: open-api.flyme.cn
Connection: keep-alive
Content-Length: 751
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
Origin: https://open-api.flyme.cn
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Linux; Android 12; DCO-AL00 Build/HUAWEIDCO-AL00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/99.0.4844.88 Mobile Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
X-Requested-With: com.meizu.smarthome
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://open-api.flyme.cn/login.do?display=login&redirect_url=https%3A%2F%2Fopen-api.flyme.cn%2Foauth%2Fauthorize%3Fresponse_type%3Dcode%26redirect_uri%3Dhttp%253A%252F%252Fwww.meizu.com%26scope%3Duc_basic_info%26client_id%3DLABG5jq09SHTWfjOSKr5
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: OSESSIONID=c8ddbb8d-cb64-4753-a487-dfc6ac80d8ae; JSESSIONID=node01udgqa8i7do6a40v8ocs6yp9l981586.node0; loginSessionId=c8ddbb8d-cb64-4753-a487-dfc6ac80d8ae; _sh_t=1693920919392; _mco=7930cd6f39357e3cdf61d0b8f36d3cbb; _kbcc=12; _mcc=7
redirect_url=https%3A%2F%2Fopen-api.flyme.cn%2Foauth%2Fauthorize%3Fresponse_type%3Dcode%26redirect_uri%3Dhttp%253A%252F%252Fwww.meizu.com%26scope%3Duc_basic_info%26client_id%3DLABG5jq09SHTWfjOSKr5&encoder_redirect_url=https%253A%252F%252Fopen-api.flyme.cn%252Foauth%252Fauthorize%253Fresponse_type%253Dcode%2526redirect_uri%253Dhttp%253A%252F%252Fwww.meizu.com%2526scope%253Duc_basic_info%2526client_id%253DLABG5jq09SHTWfjOSKr5&account=13888888888&password=XI%5b%5b%5fGZL&geetest_challenge=7e4114a5af8239e4d0baa32c4a393eb7&geetest_validate=943a2a68930df575daf228a8e1451f3c&geetest_seccode=943a2a68930df575daf228a8e1451f3c%7Cjordan&acceptFlyme=on&appuri=https%3A%2F%2Fmember.meizu.com%2Findex.jsp&useruri=&service=&cryKK=40&sid=
--------------------------------------------------------
响应内容 Response
HTTP/1.1 302
Server: nginx
Date: Tue, 05 Sep 2023 13:36:06 GMT
Content-Length: 0
Connection: keep-alive
Location: https://open-api.flyme.cn/oauth/authorize?response_type=code&redirect_uri=http%3A%2F%2Fwww.meizu.com&scope=uc_basic_info&client_id=LABG5jq09SHTWfjOSKr5
跳转认证服务器
手机端登录后跳转认证服务器链接 https://open-api.flyme.cn/oauth/authorize?response_type=code&redirect_uri=http://www.meizu.com&scope=uc_basic_info&client_id=LABG5jq09SHTWfjOSKr5 ,然后通过 302 重定向到回调网站 http://www.meizu.com ,并且带上授权临时票据 code,code 一次性使用后失效。
参数 | 是否必须 | 说明 |
---|---|---|
response_type | 是 | 返回说明,返回用于换取 access_token 的 code |
redirect_uri | 是 | 授权回调链接http://www.meizu.com |
scope | 是 | 应用授权作用域,此处填uc_basic_info |
client_id | 是 | 应用唯一标识,Lipro 的 RELEASE_CLIENT_ID 为LABG5jq09SHTWfjOSKr5 , |
返回示例
请求内容Request
GET https://open-api.flyme.cn/oauth/authorize?response_type=code&redirect_uri=http%3A%2F%2Fwww.meizu.com&scope=uc_basic_info&client_id=LABG5jq09SHTWfjOSKr5 HTTP/1.1
Host: open-api.flyme.cn
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Linux; Android 12; DCO-AL00 Build/HUAWEIDCO-AL00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/99.0.4844.88 Mobile Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
X-Requested-With: com.meizu.smarthome
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://open-api.flyme.cn/login.do?display=login&redirect_url=https%3A%2F%2Fopen-api.flyme.cn%2Foauth%2Fauthorize%3Fresponse_type%3Dcode%26redirect_uri%3Dhttp%253A%252F%252Fwww.meizu.com%26scope%3Duc_basic_info%26client_id%3DLABG5jq09SHTWfjOSKr5
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: OSESSIONID=2b1f3486-b117-4327-aca4-52055abab956; JSESSIONID=node0u9s0dfzjsrbsith5kkie1agy1016057.node0; loginSessionId=2b1f3486-b117-4327-aca4-52055abab956; _sh_t=1693999010108; _mco=7930cd6f39357e3cdf61d0b8f36d3cbb; _kbcc=29; _mcc=7
--------------------------------------------------------
响应内容Response
HTTP/1.1 302
Server: nginx
Date: Wed, 06 Sep 2023 11:22:04 GMT
Content-Length: 0
Connection: keep-alive
Pragma: no-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Cache-Control: no-cache
Cache-Control: no-store
Content-Language: zh-CN
Location: http://www.meizu.com?code=sz_ce947538b1bd4bad868db478320f9567
第二步:通过 code 获取 access_token
接口说明
获取第一步的 code 后,请求以下链接获取 access_token
请求方法
@FormUrlEncoded
@POST("https://api-lipro.meizu.com/v1/user/token/get.do")
Observable<SmartHomeResponse<GetTokenBean>> getTokenByCode(@Field("sign") String str, @Field("optFrom") String str2, @Field("optAt") long j, @Field("vn") String str3, @Field("vc") int i, @Field("model") String str4, @Field("code") String str5);
GetTokenByCode
public static Observable<SmartHomeResponse<GetTokenBean>> getTokenByCode(String str) {
Application app = getApp();
String phoneId = DeviceUtil.getPhoneId();
return getApiService().getTokenByCode(sign(phoneId), phoneId, System.currentTimeMillis(), AppUtil.getMyVersionName(app), AppUtil.getMyVersionCode(app), DeviceUtil.getFlymeModel(), str);
}
//sign=sign(phoneId),算法代码如下
private static String sign(String str) {
return Md5Util.getSign(str, "*Hilbert$@q9g");
}
//phoneId=DeviceUtil.getPhoneId(),通过获取设备信息的工具类获得
//access_token=getTokenBean.access_token
//refresh_token=getTokenBean.refresh_token
//expires_in=getTokenBean.expires_in
DeviceUtil. GetPhoneId
public class DeviceUtil {
private static final String KEY_PHONE_ID = "key_phone_id";
private static final String TAG = "SM_DeviceUtil";
private static String sPhoneId = null;
private static final String sUnAuthorizedOAID = "00000000000000000000000000000000";
public static String getPhoneId() {
try {
if (TextUtils.isEmpty(sPhoneId)) {
String decodeString = KvUtil.decodeString(KEY_PHONE_ID);
sPhoneId = decodeString;
if (TextUtils.isEmpty(decodeString)) {
sPhoneId = generatePhoneId();
}
}
LogUtil.i(TAG, "get PhoneId : " + sPhoneId);
} catch (Exception e) {
e.printStackTrace();
}
return sPhoneId;
}
//···省略
}
参数说明
参数 | 是否必须 | 说明 |
---|---|---|
sign | 是 | sign(phoneId) ,既Md5Util.getSign(phoneId, "*Hilbert$@q9g") 返函获得 |
optFrom | 是 | 唯一标识,phoneId |
optAt | 是 | 时间戳 ms |
vn | 是 | app 版本名称,AppUtil.getMyVersionName(app) 通过 AppUtil 类函数获得 |
vc | 是 | app 版本号,AppUtil.getMyVersionCode(app) 通过 AppUtil 类函数获得 |
model | 是 | 手机型号,DeviceUtil.getFlymeModel() 获取设备信息的工具类 DeviceUtil 函数获得 |
code | 是 | 填写第一步获取的 code 参数 |
返回说明
正确的返回:
参数 | 说明 |
---|---|
access_token | 接口调用凭证 |
refresh_token | 用户刷新 access_token |
xpires_in | access_token 接口调用凭证超时时间,单位(秒) |
scope | 用户授权的作用域,使用逗号(,)分隔 |
openid | 授权用户唯一标识 |
返回示例
请求内容Request
POST https://api-lipro.meizu.com/v1/user/token/get.do HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 170
Host: api-lipro.meizu.com
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.14.9
sign=9d35cdb0ffb98c6bc3e62a191c54a80d&optFrom=12cffe3b-7a62-798b-30fc-3d43d585ae22&optAt=1693920967301&vn=2.3.0&vc=2003000&model=&code=sz_e1f76335bcfa41268f635f26c175bbf7
--------------------------------------------------------
响应内容Response
HTTP/1.1 200
Server: nginx
Date: Tue, 05 Sep 2023 13:36:07 GMT
Content-Type: application/json
Connection: keep-alive
Content-Encoding: gzip
Content-Length: 315
{"code":200,"errorCode":null,"message":"","redirect":"","value":"{\"access_token\":\"eyJhIjoiNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYiLCJzIjoic3oiLCJ1IjoiMTk5OTk5OTkiLCJ2IjoxLCJpIjoiYWNjZXNzX3Rva2VuWFhYIn0\",\"token_type\":\"bearer\",\"refresh_token\":\"eyJhIjoiNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYiLCJzIjoic3oiLCJ1IjoiMTk5OTk5OTkiLCJ2IjoxLCJpIjoicmVmcmVzaF90b2tlblhYIn0\",\"expires_in\":7199,\"scope\":\"uc_basic_info\",\"open_id\":\"X_evTHdKyfVr1-vyS0EHjSOojNfh7Ap9Mf7-DAUw1XA\"}"}
错误返回样例
{
"code": 1201,
"errorCode": "AUTH_GET_TOKEN_ERROR",
"message": "org.apache.http.client.HttpResponseException: Bad Request, {\"error\":\"invalid_grant\",\"error_description\":\"Invalid authorization code: sz_e1f76335bcfa41268f635f26c175bbf7\"}",
"redirect": "",
"value": ""
}
第三步:刷新 access_token 有效期
接口说明
Access_token 是调用授权关系接口的调用凭证,由于 access_token 有效期(7199 秒)较短,当 access_token 超时后,可以使用 refresh_token 进行刷新,access_token 刷新结果有两种:
若 access_token 已超时,那么进行 refresh_token 会获取一个新的 access_token 和 refresh_token,新的超时时间。
Refresh_token 拥有较长的有效期,当 refresh_token 失效的后,需要用户重新授权。
请求方法
@FormUrlEncoded
@POST("https://api-lipro.meizu.com/v1/user/token/refresh.do")
Observable<SmartHomeResponse<GetTokenBean>> getTokenByRefreshToken(@Field("sign") String str, @Field("optFrom") String str2, @Field("optAt") long j, @Field("vn") String str3, @Field("vc") int i, @Field("model") String str4, @Field("refreshToken") String str5);
GetTokenByRefreshToken
public static Observable<SmartHomeResponse<GetTokenBean>> getTokenByRefreshToken(String str) {
Application app = getApp();
String phoneId = DeviceUtil.getPhoneId();
return getApiService().getTokenByRefreshToken(sign(phoneId), phoneId, System.currentTimeMillis(), AppUtil.getMyVersionName(app), AppUtil.getMyVersionCode(app), DeviceUtil.getFlymeModel(), str);
}
参数说明
参数 | 是否必须 | 说明 |
---|---|---|
sign | 是 | sign(phoneId) |
optFrom | 是 | 唯一标识,phoneId |
optAt | 是 | 时间戳 ms |
vn | 是 | app 版本名称,AppUtil.getMyVersionName(app) 通过 AppUtil 类函数获得 |
vc | 是 | app 版本号,AppUtil.getMyVersionCode(app) 通过 AppUtil 类函数获得 |
model | 是 | 手机型号,DeviceUtil.getFlymeModel() 获取设备信息的工具类 DeviceUtil 函数获得 |
refreshToken | 是 | 填写通过第二步 access_token 获取到的 refresh_token 参数 |
返回说明
正确的返回:
参数 | 说明 |
---|---|
access_token | 接口调用凭证 |
refresh_token | 用户刷新 access_token |
xpires_in | access_token 接口调用凭证超时时间,单位(秒) |
scope | 用户授权的作用域,使用逗号(,)分隔 |
openid | 授权用户唯一标识 |
返回示例
请求内容Request
POST https://api-lipro.meizu.com/v1/user/token/refresh.do HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 266
Host: api-lipro.meizu.com
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.14.9
sign=9d35cdb0ffb98c6bc3e62a191c54a80d&optFrom=12cffe3b-7a62-798b-30fc-3d43d585ae22&optAt=1693911004652&vn=2.3.0&vc=2003000&model=&refreshToken=eyJhIjoiNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYiLCJzIjoic3oiLCJ1IjoiMTk5OTk5OTkiLCJ2IjoxLCJpIjoicmVmcmVzaF90b2tlblhYIn0
--------------------------------------------------------
响应内容Response
HTTP/1.1 200
Server: nginx
Date: Tue, 05 Sep 2023 10:50:04 GMT
Content-Type: application/json
Connection: keep-alive
Content-Encoding: gzip
Content-Length: 315
{"code":200,"errorCode":null,"message":"","redirect":"","value":"{\"access_token\":\"eyJhIjoiNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYiLCJzIjoic3oiLCJ1IjoiMTk5OTk5OTkiLCJ2IjoxLCJpIjoiYWNjZXNzX3Rva2VuWFhYIn0\",\"token_type\":\"bearer\",\"refresh_token\":\"eyJhIjoiNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYiLCJzIjoic3oiLCJ1IjoiMTk5OTk5OTkiLCJ2IjoxLCJpIjoicmVmcmVzaF90b2tlblhYIn0\",\"expires_in\":7199,\"scope\":\"uc_basic_info\",\"open_id\":\"X_evTHdKyfVr1-vyS0EHjSOojNfh7Ap9Mf7-DAUw1XA\"}"}
错误返回样例
{
"code": 1202,
"errorCode": "AUTH_REFRESH_TOKEN_ERROR",
"message": "org.apache.http.client.HttpResponseException: Server Error, {\"error\":\"server_error\",\"error_description\":null}",
"redirect": "",
"value": ""
}
第四步:获取用户设备索引
接口说明
若已获得相关设备信息(如
"serial": "mesh_group_49666"
),可直接进入第七步操作设备请求方法
@FormUrlEncoded
@Headers({"Cache-Control: no-cache"})
@POST("https://api-lipro.meizu.com/v1/user/index.do")
Observable<SmartHomeResponse<AllDevicesBean>> fetchAllDevices(@Field("accessToken") String str, @Field("sign") String str2, @Field("optFrom") String str3, @Field("optAt") long j, @Field("vn") String str4, @Field("vc") int i, @Field("offset") int i2, @Field("limit") int i3);
public static Observable<SmartHomeResponse<AllDevicesBean>> fetchAllDevices(String str) {
Application app = getApp();
String phoneId = DeviceUtil.getPhoneId();
return getApiService().fetchAllDevices(str, sign(phoneId), phoneId, System.currentTimeMillis(), AppUtil.getMyVersionName(app), AppUtil.getMyVersionCode(app), 0, 200);
}
返回示例
请求内容Request
POST https://api-lipro.meizu.com/v1/user/index.do HTTP/1.1
Cache-Control: no-cache
Content-Type: application/x-www-form-urlencoded
Content-Length: 277
Host: api-lipro.meizu.com
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.14.9
accessToken=eyJhIjoiNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYiLCJzIjoic3oiLCJ1IjoiMTk5OTk5OTkiLCJ2IjoxLCJpIjoiYWNjZXNzX3Rva2VuWFhYIn0&sign=9d35cdb0ffb98c6bc3e62a191c54a80d&optFrom=12cffe3b-7a62-798b-30fc-3d43d585ae22&optAt=1693881745199&vn=2.3.0&vc=2003000&offset=0&limit=200
--------------------------------------------------------
响应内容Response
HTTP/1.1 200
Server: nginx
Date: Tue, 05 Sep 2023 02:42:25 GMT
Content-Type: application/json
Connection: keep-alive
Content-Encoding: gzip
Content-Length: 367
{"code":200,"errorCode":null,"message":"","redirect":"","value":{"devices":[{"deviceId":26666,"deviceName":"客厅智能灯带","group":true,"iotName":"21P3","irRemote":false,"lastModifyAt":1683097218000,"lastModifyBy":"482e84ff8d41de7b319d6bdf245e178c","model":null,"productId":11,"roomId":33331,"roomName":"客厅","serial":"mesh_group_49888","type":6,"userId":null},{"deviceId":25900,"deviceName":"主卧智能灯带","group":true,"iotName":"21P3","irRemote":false,"lastModifyAt":1682154529000,"lastModifyBy":"286bf2352b59af58","model":null,"productId":11,"roomId":33333,"roomName":"主卧","serial":"mesh_group_49666","type":6,"userId":null}],"order":{"device":",26666,25900,","room":",12121,33331,33333,"}}}
第五步:获取用户房间信息
@FormUrlEncoded
@Headers({"Cache-Control: no-cache"})
@POST("https://api-lipro.meizu.com/v1/user/room/list.do")
Observable<SmartHomeResponse<AllRoomsBean>> fetchAllRooms(@Field("accessToken") String str, @Field("sign") String str2, @Field("optFrom") String str3, @Field("optAt") long j, @Field("vn") String str4, @Field("vc") int i);
public static Observable<SmartHomeResponse<AllRoomsBean>> fetchAllRooms(String str) {
Application app = getApp();
String phoneId = DeviceUtil.getPhoneId();
return getApiService().fetchAllRooms(str, sign(phoneId), phoneId, System.currentTimeMillis(), AppUtil.getMyVersionName(app), AppUtil.getMyVersionCode(app));
}
请求内容Request
POST https://api-lipro.meizu.com/v1/user/room/list.do HTTP/1.1
Cache-Control: no-cache
Content-Type: application/x-www-form-urlencoded
Content-Length: 258
Host: api-lipro.meizu.com
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.14.9
accessToken=eyJhIjoiNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYiLCJzIjoic3oiLCJ1IjoiMTk5OTk5OTkiLCJ2IjoxLCJpIjoiYWNjZXNzX3Rva2VuWFhYIn0&sign=9d35cdb0ffb98c6bc3e62a191c54a80d&optFrom=12cffe3b-7a62-798b-30fc-3d43d585ae22&optAt=1693881745199&vn=2.3.0&vc=2003000
--------------------------------------------------------
响应内容Response
HTTP/1.1 200
Server: nginx
Date: Tue, 05 Sep 2023 02:42:25 GMT
Content-Type: application/json
Connection: keep-alive
Content-Encoding: gzip
Content-Length: 289
{"code":200,"errorCode":null,"message":"","redirect":"","value":{"order":{"device":",26666,25900,","room":",12121,33331,33333,"},"rooms":[{"def":0,"lastModifyAt":1682150606000,"lastModifyBy":"286bf2352b59af58","name":"主卧","orders":",25900,","roomId":33333},{"def":0,"lastModifyAt":1683097202000,"lastModifyBy":"482e84ff8d41de7b319d6bdf245e178c","name":"客厅","orders":",26666,","roomId":33331},{"def":1,"lastModifyAt":1683097202000,"lastModifyBy":"482e84ff8d41de7b319d6bdf245e178c","name":"未分配","orders":",","roomId":12121}]}}
第六步:获取 iot 设备状态信息
接口说明
通过 access-token 接口调用凭证获取设备状态信息,携带参数
IOT_MERCHANT_CODE
、timestamp
、sign
、RequestBody
去调用 API,其中 sign=MD 5 (access-token
+timestamp
+IOT_MERCHANT_CODE
+RequestBody
+IOT_SIGN_KEY
)请求方法
@Headers({"Cache-Control: no-cache"})
@POST("https://api-mlink.meizu.com/app/oauth/api/v2/user/query/devices/group/state.do")
Observable<IotResponse<List<IotMeshStatusBean>>> queryIotMeshGroupStatus(@Header("i-access-token") String str, @Header("merchant-code") String str2, @Header("nonce") long j, @Header("sign") String str3, @Body RequestBody requestBody);
QueryIotMeshGroupStatus
public class NetRequest {
private static final String IOT_MERCHANT_CODE = "LP0002";
private static final String IOT_SIGN_KEY = "19ff9eb20f818bc45ab216d0d67f";
//···省略
public static Observable<IotResponse<List<IotMeshStatusBean>>> iotQueryMeshGroupStatus(String str, List<String> list) {
long currentTimeMillis = System.currentTimeMillis();
String format = String.format(Locale.US, "{\"groupIdList\":[%s]}", ArrayUtil.toString(list, "\"", "\"", MzContactsContract.MzGroups.GROUP_SPLIT_MARK_EXTRA));
return getApiService().queryIotMeshGroupStatus(str, IOT_MERCHANT_CODE, currentTimeMillis, iotSign(str, IOT_MERCHANT_CODE, currentTimeMillis, format), RequestBody.create(MediaType.parse(TrackerConstants.POST_CONTENT_TYPE), format));
}
//···省略
}
参数说明
参数 | 是否必须 | 说明 |
---|---|---|
i-access-token | 是 | access-token,str |
merchant-code | 是 | IOT_MERCHANT_CODE 值为LP0002 |
nonce | 是 | 时间戳timestamp ,单位 (毫秒) |
sign | 是 | 通过 iotSign 函数返回的 MD5,Md5Util.GetSign (access-token +timestamp +IOT_MERCHANT_CODE +RequestBody +IOT_SIGN_KEY ) |
RequestBody | 是 | requestBody,format |
Iotsign
private static String iotSign(String str, String str2, long j, String str3) {
String trim = str3 != null ? str3.trim() : "";
return Md5Util.getSign(str + j + str2 + trim, IOT_SIGN_KEY);
}
//str + j + str2 + trim=`eyJhIjoiNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYiLCJzIjoic3oiLCJ1IjoiMTk5OTk5OTkiLCJ2IjoxLCJpIjoiYWNjZXNzX3Rva2VuWFhYIn0`+`1693883408434`+`LP0002`+`{"groupIdList":["mesh_group_49888","mesh_group_49666"]}`
//IOT_SIGN_KEY=`19ff9eb20f818bc45ab216d0d67f`
Md5Util. GetSign
package com.meizu.smarthome.util;
import android.annotation.SuppressLint;
import android.util.Log;
import java.security.MessageDigest;
@SuppressLint({"LogNotTimber"})
/* loaded from: classes2.dex */
public class Md5Util {
private static final String TAG = "SM_EncryptUtil";
private static final char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
public static String encodeHex(byte[] bArr) {
if (bArr == null || bArr.length <= 0) {
return null;
}
int length = bArr.length;
char[] cArr = new char[length << 1];
int i = 0;
for (int i2 = 0; i2 < length; i2++) {
int i3 = i + 1;
char[] cArr2 = hexDigits;
cArr[i] = cArr2[(bArr[i2] & 240) >>> 4];
i = i3 + 1;
cArr[i3] = cArr2[bArr[i2] & 15];
}
return new String(cArr);
}
public static String getSign(String str, String str2) {
return md5Hex((str + str2).getBytes());
}
public static String md5Hex(byte[] bArr) {
try {
return encodeHex(MessageDigest.getInstance("MD5").digest(bArr));
} catch (Exception e) {
Log.e(TAG, "", e);
return null;
}
}
}
返回说明
正确的返回:
{
"code":"0000",
"message":"success",
"data":[
{
"groupId":"mesh_group_49888",
"devices":Array[4],
"properties":Array[15],
"code":"0000",
"message":"success",
"hasBindGateway":true,
"gatewayDeviceId":null
},
{
"groupId":"mesh_group_49666",
"devices":[
{
"deviceId":"03ab5ccccccccccc",
"properties":[
{
"key":"address",
"value":"7"
},
{
"key":"meshType",
"value":"1"
},
{
"key":"groupRoot",
"value":"false"
},
{
"key":"gateway",
"value":"true"
},
{
"key":"bleMac",
"value":"5C:CD:CD:CD:CD:D8"
}
],
"status":null,
"bindCloudType":null,
"code":"0000",
"message":"success"
}
],
"properties":[
{
"key":"net_type",
"value":"-1"
},
{
"key":"wifi_rssi",
"value":"-80"
},
{
"key":"deviceName",
"value":"21P3"
},
{
"key":"gearList",
"value":"[{\"temperature\":0,\"brightness\":50},{\"temperature\":50,\"brightness\":50},{\"temperature\":100,\"brightness\":50}]"
},
{
"key":"powerState",
"value":"0"
},
{
"key":"connectState",
"value":"0"
},
{
"key":"version",
"value":"7.3.9"
},
{
"key":"switchGearOnReboot",
"value":"1"
},
{
"key":"mac",
"value":"5c:cd:cd:cd:cd:d7"
},
{
"key":"fadeState",
"value":"1"
},
{
"key":"temperature",
"value":"50"
},
{
"key":"latestSyncTimestamp",
"value":"1691640345986"
},
{
"key":"rcList",
"value":"[{\"address\":\"5ccdcdcdcd68\",\"keyindex\":1,\"name\":\"主卧无线控制器\",\"selfIndex\":0,\"timestamp\":\"1683097773619\",\"version\":\"3.1.0\"}]"
},
{
"key":"brightness",
"value":"11"
},
{
"key":"saveJson",
"value":"0"
}
],
"code":"0000",
"message":"success",
"hasBindGateway":true,
"gatewayDeviceId":null
}
],
"success":true
}
返回示例
请求内容Request
POST https://api-mlink.meizu.com/app/oauth/api/v2/user/query/devices/group/state.do HTTP/1.1
Cache-Control: no-cache
i-access-token: eyJhIjoiNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYiLCJzIjoic3oiLCJ1IjoiMTk5OTk5OTkiLCJ2IjoxLCJpIjoiYWNjZXNzX3Rva2VuWFhYIn0
merchant-code: LP0002
nonce: 1693911005137
sign: c70fb00e48c81df0c40f3fc6578fc870
Content-Type: application/json; charset=utf-8
Content-Length: 55
Host: api-mlink.meizu.com
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.14.9
{"groupIdList":["mesh_group_49888","mesh_group_49666"]}
--------------------------------------------------------
响应内容Response
HTTP/1.1 200
Server: nginx
Date: Tue, 05 Sep 2023 10:50:05 GMT
Content-Type: application/json;charset=utf-8
Connection: keep-alive
Set-Cookie: JSESSIONID=node02kbh5b8jl1lc10t4p4090ctaj23811.node0; Path=/
Set-Cookie: rememberMe=deleteMe; Path=/; Max-Age=0; Expires=Mon, 04-Sep-2023 10:50:05 GMT
Set-Cookie: rememberMe=deleteMe; Path=/; Max-Age=0; Expires=Mon, 04-Sep-2023 10:50:05 GMT
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Content-Length: 3620
{"code":"0000","message":"success","data":[{"groupId":"mesh_group_49888","devices":[{"deviceId":"03ab5cccccccccc1","properties":[{"key":"schedule","value":""},{"key":"extensions","value":"{}"},{"key":"address","value":"8"},{"key":"meshType","value":"1"},{"key":"groupRoot","value":"false"},{"key":"gateway","value":"false"},{"key":"bleMac","value":"5C:CD:CD:CD:CD:31"}],"status":null,"bindCloudType":null,"code":"0000","message":"success"},{"deviceId":"03ab5cccccccccc7","properties":[{"key":"schedule","value":""},{"key":"extensions","value":"{}"},{"key":"address","value":"9"},{"key":"meshType","value":"1"},{"key":"groupRoot","value":"false"},{"key":"gateway","value":"false"},{"key":"bleMac","value":"5C:CD:CD:CD:CD:77"}],"status":null,"bindCloudType":null,"code":"0000","message":"success"},{"deviceId":"03ab5ccccccccccd","properties":[{"key":"schedule","value":""},{"key":"extensions","value":"{}"},{"key":"address","value":"10"},{"key":"meshType","value":"1"},{"key":"groupRoot","value":"false"},{"key":"gateway","value":"false"},{"key":"bleMac","value":"5C:CD:CD:CD:CD:4D"}],"status":null,"bindCloudType":null,"code":"0000","message":"success"},{"deviceId":"03ab5cccccccccb1","properties":[{"key":"extensions","value":"{}"},{"key":"address","value":"11"},{"key":"meshType","value":"1"},{"key":"groupRoot","value":"true"},{"key":"gateway","value":"true"},{"key":"bleMac","value":"5C:CD:CD:CD:CD:B1"}],"status":null,"bindCloudType":null,"code":"0000","message":"success"}],"properties":[{"key":"net_type","value":"-1"},{"key":"wifi_rssi","value":"-39"},{"key":"deviceName","value":"21P3"},{"key":"gearList","value":"[{\"temperature\":0,\"brightness\":50},{\"temperature\":50,\"brightness\":50},{\"temperature\":100,\"brightness\":50}]"},{"key":"powerState","value":"0"},{"key":"connectState","value":"1"},{"key":"version","value":"7.3.9"},{"key":"switchGearOnReboot","value":"1"},{"key":"mac","value":"5c:CD:CD:CD:CD:b0"},{"key":"fadeState","value":"1"},{"key":"temperature","value":"50"},{"key":"latestSyncTimestamp","value":"1693888845190"},{"key":"rcList","value":"[{\"address\":\"5ccdcdcdcdf1\",\"keyindex\":1,\"name\":\"客厅智能控制器\",\"selfIndex\":0,\"timestamp\":\"1689429124762\",\"version\":\"3.1.0\"}]"},{"key":"brightness","value":"21"},{"key":"saveJson","value":"0"}],"code":"0000","message":"success","hasBindGateway":true,"gatewayDeviceId":null},{"groupId":"mesh_group_49666","devices":[{"deviceId":"03ab5ccccccccccc","properties":[{"key":"address","value":"7"},{"key":"meshType","value":"1"},{"key":"groupRoot","value":"false"},{"key":"gateway","value":"true"},{"key":"bleMac","value":"5C:CD:CD:CD:CD:D8"}],"status":null,"bindCloudType":null,"code":"0000","message":"success"}],"properties":[{"key":"net_type","value":"-1"},{"key":"wifi_rssi","value":"-80"},{"key":"deviceName","value":"21P3"},{"key":"gearList","value":"[{\"temperature\":0,\"brightness\":50},{\"temperature\":50,\"brightness\":50},{\"temperature\":100,\"brightness\":50}]"},{"key":"powerState","value":"0"},{"key":"connectState","value":"0"},{"key":"version","value":"7.3.9"},{"key":"switchGearOnReboot","value":"1"},{"key":"mac","value":"5c:cd:cd:cd:cd:d7"},{"key":"fadeState","value":"1"},{"key":"temperature","value":"50"},{"key":"latestSyncTimestamp","value":"1691640345986"},{"key":"rcList","value":"[{\"address\":\"5ccdcdcdcd68\",\"keyindex\":1,\"name\":\"主卧无线控制器\",\"selfIndex\":0,\"timestamp\":\"1683097773619\",\"version\":\"3.1.0\"}]"},{"key":"brightness","value":"11"},{"key":"saveJson","value":"0"}],"code":"0000","message":"success","hasBindGateway":true,"gatewayDeviceId":null}],"success":true}
错误返回样例
{
"error": "unauthorized_client",
"error_description": "请使用授权的access_token"
}
第七步:发送设备操作信息
接口说明
请求方法
@Headers({"Cache-Control: no-cache"})
@POST("https://api-mlink.meizu.com/app/oauth/api/v2/user/control/device/group/command.do")
Observable<IotCommandResult> iotSendCommandToMeshGroup(@Header("i-access-token") String str, @Header("merchant-code") String str2, @Header("nonce") long j, @Header("sign") String str3, @Body RequestBody requestBody);
IotSendCommandToMeshGroup
public static Observable<IotCommandResult> sendCommandToMeshGroup(String str, String str2, String str3, String str4, String str5, String str6) {
String format;
long currentTimeMillis = System.currentTimeMillis();
String iotDeviceType = getIotDeviceType(str4);
if (str6 != null && str6.length() > 0) {
format = String.format(Locale.US, "{\"command\": \"%s\", \"deviceId\": \"%s\", \"deviceType\": \"%s\", \"groupId\": \"%s\", \"properties\": %s}", str2, str3, iotDeviceType, str5, str6);
} else {
format = String.format(Locale.US, "{\"command\": \"%s\", \"deviceId\": \"%s\", \"deviceType\": \"%s\", \"groupId\": \"%s\"}", str2, str3, iotDeviceType, str5);
}
return getApiService().iotSendCommandToMeshGroup(str, IOT_MERCHANT_CODE, currentTimeMillis, iotSign(str, IOT_MERCHANT_CODE, currentTimeMillis, format), RequestBody.create(MediaType.parse(TrackerConstants.POST_CONTENT_TYPE), format));
}
参数说明
参数 | 是否必须 | 说明 |
---|---|---|
i-access-token | 是 | access-token,str |
merchant-code | 是 | IOT_MERCHANT_CODE 值为LP0002 |
nonce | 是 | 时间戳 ms |
sign | 是 | 通过 iotSign 函数返回的 MD5 |
RequestBody | 是 | requestBody,format |
RequestBod 的参数说明
参数 | 是否必须 | 说明 | |
---|---|---|---|
str | 是 | access-token | |
str2 | 是 | command 值:POWER_ON | POWER_OFF |
str3 | 是 | deviceId, | |
iotDeviceType | 是 | getIotDeviceType (str4)返回值 | |
str5 | 是 | groupId, | |
str6 | 可 | properties 值 |
返回说明
正确的返回:
{
"code":"0000",
"message":"success",
"data":{
"msgSn":"354337351310262356",
"pushSuccess":true,
"pushTimestamp":1693911007835
},
"success":true
}
返回示例
请求内容Request
POST https://api-mlink.meizu.com/app/oauth/api/v2/user/control/device/group/command.do HTTP/1.1
Cache-Control: no-cache
i-access-token: eyJhIjoiNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYiLCJzIjoic3oiLCJ1IjoiMTk5OTk5OTkiLCJ2IjoxLCJpIjoiYWNjZXNzX3Rva2VuWFhYIn0
merchant-code: LP0002
nonce: 1693911007862
sign: 5c864b4c43bcf9ac1411e3cae1443f85
Content-Type: application/json; charset=utf-8
Content-Length: 112
Host: api-mlink.meizu.com
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.14.9
{"command": "POWER_ON", "deviceId": "mesh_group_49888", "deviceType": "ff000001", "groupId": "mesh_group_49888"}
--------------------------------------------------------
响应内容Response
HTTP/1.1 200
Server: nginx
Date: Tue, 05 Sep 2023 10:50:07 GMT
Content-Type: application/json;charset=utf-8
Connection: keep-alive
Set-Cookie: JSESSIONID=node0k5mmglau1l3fjy32amjsxzl423812.node0; Path=/
Set-Cookie: rememberMe=deleteMe; Path=/; Max-Age=0; Expires=Mon, 04-Sep-2023 10:50:07 GMT
Set-Cookie: rememberMe=deleteMe; Path=/; Max-Age=0; Expires=Mon, 04-Sep-2023 10:50:07 GMT
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Content-Length: 137
{"code":"0000","message":"success","data":{"msgSn":"354337351310262356","pushSuccess":true,"pushTimestamp":1693911007835},"success":true}
错误返回样例
{
"error": "unauthorized_client",
"error_description": "请使用授权的access_token"
}
获取全部设备配置表(其他)
接口说明
获取 lipro 全部设备配置
请求方法
@FormUrlEncoded
@POST("https://api-lipro.meizu.com/v1/iot/product/index/list.do")
Observable<SmartHomeResponse<List<DeviceConfigBean>>> getDeviceConfigs(@Field("sign") String str, @Field("optFrom") String str2, @Field("optAt") long j, @Field("vn") String str3, @Field("vc") int i);
GetDeviceConfigs
public static Observable<SmartHomeResponse<List<DeviceConfigBean>>> getDeviceConfigs() {
Application app = getApp();
String phoneId = DeviceUtil.getPhoneId();
return getApiService().getDeviceConfigs(sign(phoneId), phoneId, System.currentTimeMillis(), AppUtil.getMyVersionName(app), AppUtil.getMyVersionCode(app));
}
请求内容Request
POST https://api-lipro.meizu.com/v1/iot/product/index/list.do HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 122
Host: api-lipro.meizu.com
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.14.9
sign=&optFrom=12cffe3b-7a62-798b-30fc-3d43d585ae22&optAt=1693994071436&vn=2.3.0&vc=2003000
--------------------------------------------------------
响应内容Response
HTTP/1.1 200
Server: nginx
Date: Wed, 06 Sep 2023 09:54:31 GMT
Content-Type: application/json
Connection: keep-alive
Content-Encoding: gzip
Content-Length: 3550
{"code":200,"errorCode":null,"message":"","redirect":"","value":[{"bannerUrl":"http://lipro.mzres.com/res/6c08cf2d40f328634af2b893189dd483.png","category":"照明","defaultGears":"0,50,100","iconUrl":"http://lipro.mzres.com/res/434f75364d30920f43c785c6ea58821a.png","id":1,"iotName":"20X1","iotType":"1","macRule":"(5CCDCDC[DEF][0-F][0-F][0-F][0-F])|(5CCD7C5E[0-F][0-F][0-F][0-F])|(0CDC7E1[2-4][0-F][0-F][0-F][0-F])|(5CCD7C574[0-F][0-F][0-F])|(5CCDCDCD1[0-F][0-F][0-F])|(5CCDCD714[0-F][0-F][0-F])|(5CCDCDCDD[0-F][0-F][0-F])|(5CCDCD713[0-F][0-F][0-F])|(5CCDCDCD9[0-F][0-F][0-F])|(5CCDCD72[0-F][0-F][0-F][0-F])|(5CCDCD73[0-F][0-F][0-F][0-F])|(EC4D4D9998[3-5][0-F])","maxBrightness":100,"maxTemperature":4000,"minBrightness":1,"minTemperature":3000,"minVersion":"","name":"智能吸顶灯","pairUrl":"http://lipro.mzres.com/res/17af8fc7de105e8818ded9f7b39ae064.png","skuId":"","text":"1. 开灯状态下\\n2. 使用墙壁开关,关灯再开灯,重复 6 次\\n3. 设备出现呼吸效果\\n4. 已恢复到出厂设置","type":1,"typeName":"智能吸顶灯"},{······}]}
获取用户使用 lipro 的天数(其他)
接口说明
请求方法
@FormUrlEncoded
@Headers({"Cache-Control: no-cache"})
@POST("https://api-lipro.meizu.com/v2/user/info/days/get.do")
Observable<SmartHomeResponse<UserUsageInfoBean>> queryUseLiproDays(@Field("accessToken") String str, @Field("sign") String str2, @Field("optFrom") String str3, @Field("optAt") long j, @Field("vn") String str4, @Field("vc") int i);
QueryUseLiproDays
public static Observable<SmartHomeResponse<UserUsageInfoBean>> queryUseLiproDays(String str) {
Application app = getApp();
String phoneId = DeviceUtil.getPhoneId();
return getApiService().queryUseLiproDays(str, sign(phoneId), phoneId, System.currentTimeMillis(), AppUtil.getMyVersionName(app), AppUtil.getMyVersionCode(app));
}
```bash
请求内容Request
POST https://api-lipro.meizu.com/v2/user/info/days/get.do HTTP/1.1
Cache-Control: no-cache
Content-Type: application/x-www-form-urlencoded
Content-Length: 258
Host: api-lipro.meizu.com
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.14.9
accessToken=eyJhIjoiNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYiLCJzIjoic3oiLCJ1IjoiMTk5OTk5OTkiLCJ2IjoxLCJpIjoiYWNjZXNzX3Rva2VuWFhYIn0&sign=9d35cdb0ffb98c6bc3e62a191c54a80d&optFrom=12cffe3b-7a62-798b-30fc-3d43d585ae22&optAt=1693920967848&vn=2.3.0&vc=2003000
--------------------------------------------------------
响应内容Response
HTTP/1.1 200
Server: nginx
Date: Tue, 05 Sep 2023 13:36:07 GMT
Content-Type: application/json
Content-Length: 85
Connection: keep-alive
{"code":200,"errorCode":null,"message":"","redirect":"","value":{"useLiproDays":137}}
获取用户信息(其他)
接口说明
请求方法
@FormUrlEncoded
@Headers({"Cache-Control: no-cache"})
@POST("https://api-lipro.meizu.com/v1/user/auth.do")
Observable<SmartHomeResponse<SmartHomeAuthBean>> authOnSmartHomeService(@Field("accessToken") String str, @Field("sign") String str2, @Field("optFrom") String str3, @Field("optAt") long j, @Field("vn") String str4, @Field("vc") int i);
AuthOnSmartHomeService
public static Observable<SmartHomeResponse<SmartHomeAuthBean>> authOnSmartHomeService(String str) {
Application app = getApp();
String phoneId = DeviceUtil.getPhoneId();
return getApiService().authOnSmartHomeService(str, sign(phoneId), phoneId, System.currentTimeMillis(), AppUtil.getMyVersionName(app), AppUtil.getMyVersionCode(app));
}
请求内容Request
POST https://api-lipro.meizu.com/v1/user/auth.do HTTP/1.1
Cache-Control: no-cache
Content-Type: application/x-www-form-urlencoded
Content-Length: 258
Host: api-lipro.meizu.com
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.14.9
accessToken=eyJhIjoiNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYiLCJzIjoic3oiLCJ1IjoiMTk5OTk5OTkiLCJ2IjoxLCJpIjoiYWNjZXNzX3Rva2VuWFhYIn0&sign=9d35cdb0ffb98c6bc3e62a191c54a80d&optFrom=12cffe3b-7a62-798b-30fc-3d43d585ae22&optAt=1693920967605&vn=2.3.0&vc=2003000
--------------------------------------------------------
响应内容Response
HTTP/1.1 200
Server: nginx
Date: Tue, 05 Sep 2023 13:36:07 GMT
Content-Type: application/json
Content-Length: 125
Connection: keep-alive
{"code":200,"errorCode":null,"message":"","redirect":"","value":{"uid":19461698,"source":1,"status":1,"regAt":1671866844000}}