mijiaAPI 1.5.0__tar.gz → 2.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mijiaapi-1.5.0 → mijiaapi-2.0.1}/PKG-INFO +31 -5
- {mijiaapi-1.5.0 → mijiaapi-2.0.1}/README.md +30 -4
- mijiaapi-2.0.1/mijiaAPI/__init__.py +3 -0
- {mijiaapi-1.5.0 → mijiaapi-2.0.1}/mijiaAPI/__main__.py +27 -42
- {mijiaapi-1.5.0 → mijiaapi-2.0.1}/mijiaAPI/apis.py +80 -17
- mijiaapi-1.5.0/mijiaAPI/urls.py → mijiaapi-2.0.1/mijiaAPI/consts.py +2 -0
- {mijiaapi-1.5.0 → mijiaapi-2.0.1}/mijiaAPI/devices.py +47 -100
- {mijiaapi-1.5.0 → mijiaapi-2.0.1}/mijiaAPI/login.py +25 -22
- {mijiaapi-1.5.0 → mijiaapi-2.0.1}/mijiaAPI/utils.py +2 -3
- {mijiaapi-1.5.0 → mijiaapi-2.0.1}/pyproject.toml +2 -2
- mijiaapi-1.5.0/mijiaAPI/__init__.py +0 -3
- {mijiaapi-1.5.0 → mijiaapi-2.0.1}/LICENSE +0 -0
- {mijiaapi-1.5.0 → mijiaapi-2.0.1}/mijiaAPI/code.py +0 -0
- {mijiaapi-1.5.0 → mijiaapi-2.0.1}/mijiaAPI/logger.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: mijiaAPI
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.1
|
|
4
4
|
Summary: A Python API for Xiaomi Mijia
|
|
5
5
|
License: GPLv3
|
|
6
6
|
Author: Do1e
|
|
@@ -30,6 +30,12 @@ Description-Content-Type: text/markdown
|
|
|
30
30
|
[](https://pypi.org/project/mijiaAPI/)
|
|
31
31
|
[](https://opensource.org/licenses/GPL-3.0)
|
|
32
32
|
|
|
33
|
+
## ⚠️ 重要提醒
|
|
34
|
+
|
|
35
|
+
**自 v1.5.0 版本以来,本项目包含多项破坏性变更!**
|
|
36
|
+
|
|
37
|
+
如果您正在从旧版本升级,请务必查看 [CHANGELOG.md](CHANGELOG.md) 以了解详细的变更内容和迁移指南。
|
|
38
|
+
|
|
33
39
|
## 安装
|
|
34
40
|
|
|
35
41
|
### 从 PyPI 安装(推荐)
|
|
@@ -91,15 +97,18 @@ yay -S python-mijia-api
|
|
|
91
97
|
|
|
92
98
|
#### 设备与场景获取与控制:
|
|
93
99
|
|
|
100
|
+
下述方法可参考 [demos/test_apis.py](demos/test_apis.py) 中的示例。
|
|
101
|
+
|
|
94
102
|
* `get_devices_list() -> list`:获取设备列表
|
|
95
103
|
* `get_homes_list() -> list`:获取家庭列表(包含房间信息)
|
|
96
104
|
* `get_scenes_list(home_id: str) -> list`:获取手动场景列表
|
|
97
105
|
- 在米家 App 中通过 **米家→添加→手动控制** 设置
|
|
98
106
|
* `run_scene(scene_id: str) -> bool`:运行指定场景
|
|
99
|
-
* `get_consumable_items(home_id: str) -> list
|
|
107
|
+
* `get_consumable_items(home_id: str, owner_id: Optional[int] = None) -> list`:获取设备的耗材信息,如果是共享家庭,需要额外指定 `owner_id` 参数
|
|
100
108
|
* `get_devices_prop(data: list) -> list`:获取设备属性
|
|
101
109
|
* `set_devices_prop(data: list) -> list`:设置设备属性
|
|
102
110
|
* `run_action(data: dict) -> dict`:执行设备的特定动作
|
|
111
|
+
* `get_statistics(data: dict) -> list`:获取设备的统计信息,如空调每个月的耗电量,参考 [demos/test_get_statistics.py](demos/test_get_statistics.py)
|
|
103
112
|
|
|
104
113
|
设备属性和动作的相关参数(`siid`, `piid`, `aiid`)可以从 [米家产品库](https://home.miot-spec.com) 查询:
|
|
105
114
|
* 访问 `https://home.miot-spec.com/spec/{model}`(`model` 在设备列表中获取)
|
|
@@ -145,9 +154,9 @@ mijiaDevice(api: mijiaAPI, dev_info: dict = None, dev_name: str = None, did: str
|
|
|
145
154
|
|
|
146
155
|
#### 使用方法控制:
|
|
147
156
|
|
|
148
|
-
* `set(name: str,
|
|
149
|
-
* `get(name: str, did: str) -> Union[bool, int, float, str]`:获取设备属性
|
|
150
|
-
* `run_action(name: str, did: str = None, value:
|
|
157
|
+
* `set(name: str, value: Union[bool, int, float, str], did: Optional[str] = None) -> bool`:设置设备属性
|
|
158
|
+
* `get(name: str, did: Optional[str] = None) -> Union[bool, int, float, str]`:获取设备属性
|
|
159
|
+
* `run_action(name: str, did: Optional[str] = None, value: Optional[Union[list, tuple]] = None, **kwargs) -> bool`:执行设备动作
|
|
151
160
|
|
|
152
161
|
#### 属性样式访问:
|
|
153
162
|
|
|
@@ -255,6 +264,23 @@ mijiaAPI --run 明天天气如何
|
|
|
255
264
|
mijiaAPI --run 打开台灯并将亮度调至最大 --quiet
|
|
256
265
|
```
|
|
257
266
|
|
|
267
|
+
## 常见问题
|
|
268
|
+
|
|
269
|
+
### 账号密码登录失败
|
|
270
|
+
|
|
271
|
+
现在登录似乎100%遇到验证码,建议使用扫码登录。
|
|
272
|
+
|
|
273
|
+
### XXX设备的XXX如何获取/设置
|
|
274
|
+
|
|
275
|
+
我拥有的设备有限,无法保证能解答这类问题,但也欢迎提交 [issue](https://github.com/Do1e/mijia-api/issues),可能需要你将设备共享给我进行抓包或者自行抓包给我提供请求和响应,提供har文件的话注意自行删除cookie等敏感信息。
|
|
276
|
+
|
|
277
|
+
### 如何抓包
|
|
278
|
+
|
|
279
|
+
小米官方给了一个[抓包教程](https://iot.mi.com/new/doc/accesses/direct-access/extension-development/troubleshooting/packet_capture),我没试过,不确定是否能行,如果抓包成功数据是加密的,可以使用 [demos/decrypt.py](demos/decrypt.py) 解密。
|
|
280
|
+
|
|
281
|
+
我自己的解决方案是使用一个获取了root的手机,安装 [reqable](https://reqable.com/zh-CN/) 进行抓包,导出 HAR 文件后使用 [demos/decrypt_har.py](demos/decrypt_har.py) 解密。
|
|
282
|
+
|
|
283
|
+
|
|
258
284
|
## 致谢
|
|
259
285
|
|
|
260
286
|
* [janzlan/mijia-api](https://gitee.com/janzlan/mijia-api/tree/master)
|
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
[](https://pypi.org/project/mijiaAPI/)
|
|
7
7
|
[](https://opensource.org/licenses/GPL-3.0)
|
|
8
8
|
|
|
9
|
+
## ⚠️ 重要提醒
|
|
10
|
+
|
|
11
|
+
**自 v1.5.0 版本以来,本项目包含多项破坏性变更!**
|
|
12
|
+
|
|
13
|
+
如果您正在从旧版本升级,请务必查看 [CHANGELOG.md](CHANGELOG.md) 以了解详细的变更内容和迁移指南。
|
|
14
|
+
|
|
9
15
|
## 安装
|
|
10
16
|
|
|
11
17
|
### 从 PyPI 安装(推荐)
|
|
@@ -67,15 +73,18 @@ yay -S python-mijia-api
|
|
|
67
73
|
|
|
68
74
|
#### 设备与场景获取与控制:
|
|
69
75
|
|
|
76
|
+
下述方法可参考 [demos/test_apis.py](demos/test_apis.py) 中的示例。
|
|
77
|
+
|
|
70
78
|
* `get_devices_list() -> list`:获取设备列表
|
|
71
79
|
* `get_homes_list() -> list`:获取家庭列表(包含房间信息)
|
|
72
80
|
* `get_scenes_list(home_id: str) -> list`:获取手动场景列表
|
|
73
81
|
- 在米家 App 中通过 **米家→添加→手动控制** 设置
|
|
74
82
|
* `run_scene(scene_id: str) -> bool`:运行指定场景
|
|
75
|
-
* `get_consumable_items(home_id: str) -> list
|
|
83
|
+
* `get_consumable_items(home_id: str, owner_id: Optional[int] = None) -> list`:获取设备的耗材信息,如果是共享家庭,需要额外指定 `owner_id` 参数
|
|
76
84
|
* `get_devices_prop(data: list) -> list`:获取设备属性
|
|
77
85
|
* `set_devices_prop(data: list) -> list`:设置设备属性
|
|
78
86
|
* `run_action(data: dict) -> dict`:执行设备的特定动作
|
|
87
|
+
* `get_statistics(data: dict) -> list`:获取设备的统计信息,如空调每个月的耗电量,参考 [demos/test_get_statistics.py](demos/test_get_statistics.py)
|
|
79
88
|
|
|
80
89
|
设备属性和动作的相关参数(`siid`, `piid`, `aiid`)可以从 [米家产品库](https://home.miot-spec.com) 查询:
|
|
81
90
|
* 访问 `https://home.miot-spec.com/spec/{model}`(`model` 在设备列表中获取)
|
|
@@ -121,9 +130,9 @@ mijiaDevice(api: mijiaAPI, dev_info: dict = None, dev_name: str = None, did: str
|
|
|
121
130
|
|
|
122
131
|
#### 使用方法控制:
|
|
123
132
|
|
|
124
|
-
* `set(name: str,
|
|
125
|
-
* `get(name: str, did: str) -> Union[bool, int, float, str]`:获取设备属性
|
|
126
|
-
* `run_action(name: str, did: str = None, value:
|
|
133
|
+
* `set(name: str, value: Union[bool, int, float, str], did: Optional[str] = None) -> bool`:设置设备属性
|
|
134
|
+
* `get(name: str, did: Optional[str] = None) -> Union[bool, int, float, str]`:获取设备属性
|
|
135
|
+
* `run_action(name: str, did: Optional[str] = None, value: Optional[Union[list, tuple]] = None, **kwargs) -> bool`:执行设备动作
|
|
127
136
|
|
|
128
137
|
#### 属性样式访问:
|
|
129
138
|
|
|
@@ -231,6 +240,23 @@ mijiaAPI --run 明天天气如何
|
|
|
231
240
|
mijiaAPI --run 打开台灯并将亮度调至最大 --quiet
|
|
232
241
|
```
|
|
233
242
|
|
|
243
|
+
## 常见问题
|
|
244
|
+
|
|
245
|
+
### 账号密码登录失败
|
|
246
|
+
|
|
247
|
+
现在登录似乎100%遇到验证码,建议使用扫码登录。
|
|
248
|
+
|
|
249
|
+
### XXX设备的XXX如何获取/设置
|
|
250
|
+
|
|
251
|
+
我拥有的设备有限,无法保证能解答这类问题,但也欢迎提交 [issue](https://github.com/Do1e/mijia-api/issues),可能需要你将设备共享给我进行抓包或者自行抓包给我提供请求和响应,提供har文件的话注意自行删除cookie等敏感信息。
|
|
252
|
+
|
|
253
|
+
### 如何抓包
|
|
254
|
+
|
|
255
|
+
小米官方给了一个[抓包教程](https://iot.mi.com/new/doc/accesses/direct-access/extension-development/troubleshooting/packet_capture),我没试过,不确定是否能行,如果抓包成功数据是加密的,可以使用 [demos/decrypt.py](demos/decrypt.py) 解密。
|
|
256
|
+
|
|
257
|
+
我自己的解决方案是使用一个获取了root的手机,安装 [reqable](https://reqable.com/zh-CN/) 进行抓包,导出 HAR 文件后使用 [demos/decrypt_har.py](demos/decrypt_har.py) 解密。
|
|
258
|
+
|
|
259
|
+
|
|
234
260
|
## 致谢
|
|
235
261
|
|
|
236
262
|
* [janzlan/mijia-api](https://gitee.com/janzlan/mijia-api/tree/master)
|
|
@@ -133,7 +133,7 @@ def init_api(auth_path: str) -> mijiaAPI:
|
|
|
133
133
|
auth = json.load(f)
|
|
134
134
|
api = mijiaAPI(auth_data=auth)
|
|
135
135
|
if not api.available:
|
|
136
|
-
raise ValueError("
|
|
136
|
+
raise ValueError("认证信息已过期")
|
|
137
137
|
except (json.JSONDecodeError, ValueError):
|
|
138
138
|
api = mijiaLogin(save_path=auth_path)
|
|
139
139
|
auth = api.QRlogin()
|
|
@@ -146,12 +146,8 @@ def init_api(auth_path: str) -> mijiaAPI:
|
|
|
146
146
|
|
|
147
147
|
def get_devices_list(api: mijiaAPI, verbose: bool = True) -> dict:
|
|
148
148
|
devices = api.get_devices_list()
|
|
149
|
-
if 'list' in devices:
|
|
150
|
-
devices = devices['list']
|
|
151
|
-
else:
|
|
152
|
-
devices = []
|
|
153
149
|
if verbose:
|
|
154
|
-
print("
|
|
150
|
+
print("设备列表:")
|
|
155
151
|
for device in devices:
|
|
156
152
|
print(f" - {device['name']}\n"
|
|
157
153
|
f" did: {device['did']}\n"
|
|
@@ -165,19 +161,15 @@ def get_homes_list(api: mijiaAPI, verbose: bool = True, device_mapping: Optional
|
|
|
165
161
|
if device_mapping is None:
|
|
166
162
|
device_mapping = get_devices_list(api, verbose=False)
|
|
167
163
|
homes = api.get_homes_list()
|
|
168
|
-
if 'homelist' in homes:
|
|
169
|
-
homes = homes['homelist']
|
|
170
|
-
else:
|
|
171
|
-
homes = []
|
|
172
164
|
if verbose:
|
|
173
|
-
print("
|
|
165
|
+
print("家庭列表:")
|
|
174
166
|
for home in homes:
|
|
175
167
|
print(f" - {home['name']}\n"
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
print("
|
|
168
|
+
f" ID: {home['id']}\n"
|
|
169
|
+
f" 地址: {home['address']}\n"
|
|
170
|
+
f" 房间数量: {len(home['roomlist'])}\n"
|
|
171
|
+
f" 创建时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(home['create_time']))}")
|
|
172
|
+
print( " 房间列表:")
|
|
181
173
|
for room in home['roomlist']:
|
|
182
174
|
devices_name = []
|
|
183
175
|
if room['dids']:
|
|
@@ -188,9 +180,9 @@ def get_homes_list(api: mijiaAPI, verbose: bool = True, device_mapping: Optional
|
|
|
188
180
|
devices_name.append(did)
|
|
189
181
|
dids = ', '.join(devices_name)
|
|
190
182
|
print(f" - {room['name']}\n"
|
|
191
|
-
f"
|
|
192
|
-
f"
|
|
193
|
-
f"
|
|
183
|
+
f" ID: {room['id']}\n"
|
|
184
|
+
f" 设备列表: {dids}\n"
|
|
185
|
+
f" 创建时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(room['create_time']))}")
|
|
194
186
|
home_mapping = {home['id']: home for home in homes}
|
|
195
187
|
return home_mapping
|
|
196
188
|
|
|
@@ -200,16 +192,12 @@ def get_scenes_list(api: mijiaAPI, verbose: bool = True, home_mapping: Optional[
|
|
|
200
192
|
scene_mapping = {}
|
|
201
193
|
for home_id, home in home_mapping.items():
|
|
202
194
|
scenes = api.get_scenes_list(home_id)
|
|
203
|
-
if 'scene_info_list' in scenes:
|
|
204
|
-
scenes = scenes['scene_info_list']
|
|
205
|
-
else:
|
|
206
|
-
scenes = []
|
|
207
195
|
if scenes and verbose:
|
|
208
|
-
print(f"
|
|
196
|
+
print(f"{home['name']} ({home_id}) 中的场景:")
|
|
209
197
|
for scene in scenes:
|
|
210
198
|
print(f" - {scene['name']}\n"
|
|
211
|
-
f"
|
|
212
|
-
f"
|
|
199
|
+
f" ID: {scene['scene_id']}\n"
|
|
200
|
+
f" 创建时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(int(scene['create_time'])))}\n"
|
|
213
201
|
f" update time: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(int(scene['update_time'])))}")
|
|
214
202
|
scene_mapping.update({scene['scene_id']: scene for scene in scenes})
|
|
215
203
|
return scene_mapping
|
|
@@ -218,15 +206,12 @@ def get_consumable_items(api: mijiaAPI, home_mapping: Optional[dict] = None):
|
|
|
218
206
|
if home_mapping is None:
|
|
219
207
|
home_mapping = get_homes_list(api, verbose=False)
|
|
220
208
|
for home_id, home in home_mapping.items():
|
|
221
|
-
items = api.get_consumable_items(home_id)
|
|
222
|
-
|
|
223
|
-
items = items['items'][0]['consumes_data']
|
|
224
|
-
else:
|
|
225
|
-
items = []
|
|
226
|
-
print(f"Consumable items in {home['name']} ({home_id}):")
|
|
209
|
+
items = api.get_consumable_items(home_id, home['uid'])
|
|
210
|
+
print(f"{home['name']} ({home_id}) 中的耗材:")
|
|
227
211
|
for item in items:
|
|
228
|
-
|
|
229
|
-
|
|
212
|
+
for consumes_data in item['consumes_data']:
|
|
213
|
+
print(f" - {consumes_data['details'][0]['description']} 在 {consumes_data['name']}({consumes_data['did']})\n"
|
|
214
|
+
f" 值: {consumes_data['details'][0]['value']}")
|
|
230
215
|
|
|
231
216
|
def run_scene(api: mijiaAPI, scene_id: str, scene_mapping: Optional[dict] = None) -> bool:
|
|
232
217
|
if scene_mapping is None:
|
|
@@ -240,15 +225,15 @@ def run_scene(api: mijiaAPI, scene_id: str, scene_mapping: Optional[dict] = None
|
|
|
240
225
|
found = True
|
|
241
226
|
break
|
|
242
227
|
if not found:
|
|
243
|
-
print(f"
|
|
228
|
+
print(f"场景 {scene_name_to_find} 未找到")
|
|
244
229
|
return False
|
|
245
230
|
scene_name = scene_mapping[scene_id]['name']
|
|
246
231
|
ret = api.run_scene(scene_id)
|
|
247
232
|
if ret:
|
|
248
|
-
print(f"
|
|
233
|
+
print(f"场景 {scene_name}({scene_id}) 运行成功")
|
|
249
234
|
return True
|
|
250
235
|
else:
|
|
251
|
-
print(f"
|
|
236
|
+
print(f"运行场景 {scene_name}({scene_id}) 失败")
|
|
252
237
|
return False
|
|
253
238
|
|
|
254
239
|
def get(args):
|
|
@@ -256,17 +241,17 @@ def get(args):
|
|
|
256
241
|
device = mijiaDevice(api, dev_name=args.dev_name)
|
|
257
242
|
value = device.get(args.prop_name)
|
|
258
243
|
unit = device.prop_list[args.prop_name].unit
|
|
259
|
-
print(f"
|
|
244
|
+
print(f"{args.dev_name} 的 {args.prop_name} 值为 {value} {unit if unit else ''}")
|
|
260
245
|
|
|
261
246
|
def set(args):
|
|
262
247
|
api = init_api(args.auth_path)
|
|
263
248
|
device = mijiaDevice(api, dev_name=args.dev_name)
|
|
264
|
-
ret = device.
|
|
249
|
+
ret = device.set(args.prop_name, args.value)
|
|
265
250
|
unit = device.prop_list[args.prop_name].unit
|
|
266
251
|
if ret:
|
|
267
|
-
print(f"
|
|
252
|
+
print(f"{args.dev_name} 的 {args.prop_name} 值已设置为 {args.value} {unit if unit else ''}")
|
|
268
253
|
else:
|
|
269
|
-
print(f"
|
|
254
|
+
print(f"设置 {args.dev_name} 的 {args.prop_name} 值为 {args.value} 失败")
|
|
270
255
|
|
|
271
256
|
|
|
272
257
|
def main(args):
|
|
@@ -310,7 +295,7 @@ def main(args):
|
|
|
310
295
|
wifispeaker = mijiaDevice(api, dev_name=device['name'])
|
|
311
296
|
break
|
|
312
297
|
if wifispeaker is None:
|
|
313
|
-
raise ValueError("
|
|
298
|
+
raise ValueError("未找到小爱音箱设备")
|
|
314
299
|
else:
|
|
315
300
|
wifispeaker = mijiaDevice(api, dev_name=args.wifispeaker_name)
|
|
316
301
|
wifispeaker.run_action('execute-text-directive', _in=[args.run, args.quiet])
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
-
from typing import Union
|
|
2
|
+
from typing import Union, Optional
|
|
3
3
|
|
|
4
4
|
import requests
|
|
5
5
|
import requests.cookies
|
|
6
6
|
|
|
7
|
-
from .
|
|
7
|
+
from .consts import defaultUA
|
|
8
|
+
from .utils import post_data
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class mijiaAPI(object):
|
|
@@ -19,7 +20,7 @@ class mijiaAPI(object):
|
|
|
19
20
|
Exception: 当授权数据不完整时抛出异常。
|
|
20
21
|
"""
|
|
21
22
|
if any(k not in auth_data for k in ['userId', 'deviceId', 'ssecurity', 'serviceToken']):
|
|
22
|
-
raise Exception('
|
|
23
|
+
raise Exception('授权数据无效')
|
|
23
24
|
self.userId = auth_data['userId']
|
|
24
25
|
self.ssecurity = auth_data['ssecurity']
|
|
25
26
|
self.session = requests.Session()
|
|
@@ -35,7 +36,7 @@ class mijiaAPI(object):
|
|
|
35
36
|
@staticmethod
|
|
36
37
|
def _post_process(data: dict) -> Union[list, bool]:
|
|
37
38
|
if data['code'] != 0:
|
|
38
|
-
raise Exception(f'
|
|
39
|
+
raise Exception(f'获取数据失败, {data["message"]}')
|
|
39
40
|
return data['result']
|
|
40
41
|
|
|
41
42
|
@property
|
|
@@ -53,16 +54,38 @@ class mijiaAPI(object):
|
|
|
53
54
|
return True
|
|
54
55
|
return False
|
|
55
56
|
|
|
56
|
-
def get_devices_list(self) ->
|
|
57
|
+
def get_devices_list(self) -> list:
|
|
57
58
|
"""
|
|
58
59
|
获取设备列表。
|
|
59
60
|
|
|
60
61
|
Returns:
|
|
61
62
|
dict: 设备列表。
|
|
62
63
|
"""
|
|
63
|
-
uri = '/home/
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
uri = '/home/home_device_list'
|
|
65
|
+
home_list = self.get_homes_list()
|
|
66
|
+
devices = []
|
|
67
|
+
for home in home_list:
|
|
68
|
+
start_did = ''
|
|
69
|
+
has_more = True
|
|
70
|
+
while has_more:
|
|
71
|
+
data = {
|
|
72
|
+
"home_owner": home['uid'],
|
|
73
|
+
"home_id": int(home['id']),
|
|
74
|
+
"limit": 200,
|
|
75
|
+
"start_did": start_did,
|
|
76
|
+
"get_split_device": True,
|
|
77
|
+
"support_smart_home": True,
|
|
78
|
+
"get_cariot_device": True,
|
|
79
|
+
"get_third_device": True
|
|
80
|
+
}
|
|
81
|
+
ret = self._post_process(post_data(self.session, self.ssecurity, uri, data))
|
|
82
|
+
if ret and ret.get('device_info'):
|
|
83
|
+
devices.extend(ret['device_info'])
|
|
84
|
+
start_did = ret.get('max_did', '')
|
|
85
|
+
has_more = ret.get('has_more', False) and start_did != ''
|
|
86
|
+
else:
|
|
87
|
+
has_more = False
|
|
88
|
+
return devices
|
|
66
89
|
|
|
67
90
|
def get_homes_list(self) -> list:
|
|
68
91
|
"""
|
|
@@ -71,9 +94,9 @@ class mijiaAPI(object):
|
|
|
71
94
|
Returns:
|
|
72
95
|
list: 家庭列表,包括房间信息。
|
|
73
96
|
"""
|
|
74
|
-
uri = '/v2/homeroom/
|
|
75
|
-
data = {"fg":
|
|
76
|
-
return self._post_process(post_data(self.session, self.ssecurity, uri, data))
|
|
97
|
+
uri = '/v2/homeroom/gethome_merged'
|
|
98
|
+
data = {"fg": True, "fetch_share": True, "fetch_share_dev": True, "limit": 300, "app_ver": 7}
|
|
99
|
+
return self._post_process(post_data(self.session, self.ssecurity, uri, data))['homelist']
|
|
77
100
|
|
|
78
101
|
def get_scenes_list(self, home_id: str) -> list:
|
|
79
102
|
"""
|
|
@@ -89,7 +112,10 @@ class mijiaAPI(object):
|
|
|
89
112
|
"""
|
|
90
113
|
uri = '/appgateway/miot/appsceneservice/AppSceneService/GetSceneList'
|
|
91
114
|
data = {"home_id": home_id}
|
|
92
|
-
|
|
115
|
+
ret = self._post_process(post_data(self.session, self.ssecurity, uri, data))
|
|
116
|
+
if ret and 'scene_info_list' in ret:
|
|
117
|
+
return ret['scene_info_list']
|
|
118
|
+
return []
|
|
93
119
|
|
|
94
120
|
def run_scene(self, scene_id: str) -> bool:
|
|
95
121
|
"""
|
|
@@ -105,19 +131,23 @@ class mijiaAPI(object):
|
|
|
105
131
|
data = {"scene_id": scene_id, "trigger_key": "user.click"}
|
|
106
132
|
return self._post_process(post_data(self.session, self.ssecurity, uri, data))
|
|
107
133
|
|
|
108
|
-
def get_consumable_items(self, home_id: str) -> list:
|
|
134
|
+
def get_consumable_items(self, home_id: str, owner_id: Optional[int] = None) -> list:
|
|
109
135
|
"""
|
|
110
136
|
获取耗材列表。
|
|
111
137
|
|
|
112
138
|
Args:
|
|
113
139
|
home_id (str): 家庭ID,从get_homes_list获取。
|
|
140
|
+
owner_id (str, optional): 用户ID,默认为None,如果`home_id`为共享家庭,则需要提供owner_id。
|
|
114
141
|
|
|
115
142
|
Returns:
|
|
116
143
|
list: 耗材列表。
|
|
117
144
|
"""
|
|
118
145
|
uri = '/v2/home/standard_consumable_items'
|
|
119
|
-
data = {"home_id": int(home_id), "owner_id": self.userId}
|
|
120
|
-
|
|
146
|
+
data = {"home_id": int(home_id), "owner_id": int(owner_id) if owner_id else self.userId}
|
|
147
|
+
ret = self._post_process(post_data(self.session, self.ssecurity, uri, data))
|
|
148
|
+
if ret and 'items' in ret:
|
|
149
|
+
return ret['items']
|
|
150
|
+
return []
|
|
121
151
|
|
|
122
152
|
def get_devices_prop(self, data: list) -> list:
|
|
123
153
|
"""
|
|
@@ -155,7 +185,7 @@ class mijiaAPI(object):
|
|
|
155
185
|
|
|
156
186
|
示例(yeelink.light.lamp4):
|
|
157
187
|
[
|
|
158
|
-
{"did": "1234567890", "siid": 2, "piid": 2, "value": 50}
|
|
188
|
+
{"did": "1234567890", "siid": 2, "piid": 2, "value": 50}, # 设置亮度为50%
|
|
159
189
|
{"did": "1234567890", "siid": 2, "piid": 3, "value": 2700} # 设置色温为2700K
|
|
160
190
|
]
|
|
161
191
|
|
|
@@ -178,7 +208,7 @@ class mijiaAPI(object):
|
|
|
178
208
|
- value: 参数列表
|
|
179
209
|
|
|
180
210
|
示例(xiaomi.feeder.pi2001):
|
|
181
|
-
{"did": "1234567890", "siid": 2, "aiid": 1, "value": [2]}
|
|
211
|
+
{"did": "1234567890", "siid": 2, "aiid": 1, "value": [2]} # 远程喂食2份
|
|
182
212
|
|
|
183
213
|
Returns:
|
|
184
214
|
dict: 操作结果。
|
|
@@ -186,3 +216,36 @@ class mijiaAPI(object):
|
|
|
186
216
|
uri = '/miotspec/action'
|
|
187
217
|
data = {"params": data}
|
|
188
218
|
return self._post_process(post_data(self.session, self.ssecurity, uri, data))
|
|
219
|
+
|
|
220
|
+
def get_statistics(self, data: dict) -> list:
|
|
221
|
+
"""
|
|
222
|
+
获取设备的统计信息。
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
data (dict): 请求参数,包含以下键:
|
|
226
|
+
- did: 设备ID,从get_devices_list获取
|
|
227
|
+
- key: siid.piid,表示要获取统计数据的属性
|
|
228
|
+
- data_type: 统计类型,可选值包括:
|
|
229
|
+
- 'stat_hour_v3': 按小时统计
|
|
230
|
+
- 'stat_day_v3': 按天统计
|
|
231
|
+
- 'stat_week_v3': 按周统计
|
|
232
|
+
- 'stat_month_v3': 按月统计
|
|
233
|
+
- limit: 返回的最大条目数,可选参数
|
|
234
|
+
- time_start: 开始时间戳,单位为秒
|
|
235
|
+
- time_end: 结束时间戳,单位为秒
|
|
236
|
+
|
|
237
|
+
示例(lumi.acpartner.mcn04 的 power-consumption):
|
|
238
|
+
{
|
|
239
|
+
"did": "1234567890",
|
|
240
|
+
"key": "7.1",
|
|
241
|
+
"data_type": "stat_month_v3",
|
|
242
|
+
"limit": 24,
|
|
243
|
+
"time_start": 1685548800,
|
|
244
|
+
"time_end": 1750694400,
|
|
245
|
+
} # 2023-06-01 00:00:00 到 2025-06-24 00:00:00 的月度统计数据
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
list: 统计信息列表。
|
|
249
|
+
"""
|
|
250
|
+
uri = '/v2/user/statistics'
|
|
251
|
+
return self._post_process(post_data(self.session, self.ssecurity, uri, data))
|
|
@@ -5,3 +5,5 @@ qrURL = 'https://account.xiaomi.com/longPolling/loginUrl'
|
|
|
5
5
|
apiURL = 'https://api.io.mi.com/app'
|
|
6
6
|
deviceURL = 'https://home.miot-spec.com/spec/'
|
|
7
7
|
accountURL = 'https://account.xiaomi.com/pass2/profile/home?bizFlag=&userId='
|
|
8
|
+
|
|
9
|
+
defaultUA = 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36 Edg/126.0.0.0'
|
|
@@ -6,7 +6,7 @@ import requests
|
|
|
6
6
|
from time import sleep
|
|
7
7
|
from .apis import mijiaAPI
|
|
8
8
|
from .code import ERROR_CODE
|
|
9
|
-
from .
|
|
9
|
+
from .consts import deviceURL
|
|
10
10
|
from .logger import get_logger
|
|
11
11
|
|
|
12
12
|
logger = get_logger(__name__)
|
|
@@ -26,7 +26,7 @@ class DevProp(object):
|
|
|
26
26
|
self.desc = prop_dict['description']
|
|
27
27
|
self.type = prop_dict['type']
|
|
28
28
|
if self.type not in ['bool', 'int', 'uint', 'float', 'string']:
|
|
29
|
-
raise ValueError(f'
|
|
29
|
+
raise ValueError(f'不支持的类型: {self.type}, 可选类型: bool, int, uint, float, string')
|
|
30
30
|
self.rw = prop_dict['rw']
|
|
31
31
|
self.unit = prop_dict['unit']
|
|
32
32
|
self.range = prop_dict['range']
|
|
@@ -106,18 +106,18 @@ class mijiaDevice(object):
|
|
|
106
106
|
- 如果只提供了dev_info,则直接使用该信息。
|
|
107
107
|
"""
|
|
108
108
|
if dev_info is None and dev_name is None:
|
|
109
|
-
raise RuntimeError("
|
|
109
|
+
raise RuntimeError("必须提供 'dev_info' 或 'dev_name' 中的一个参数。")
|
|
110
110
|
if dev_info is not None and dev_name is not None:
|
|
111
|
-
logger.warning("
|
|
111
|
+
logger.warning("同时提供了 'dev_info' 和 'dev_name'。将使用 'dev_info' 进行初始化。")
|
|
112
112
|
|
|
113
113
|
self.api = api
|
|
114
114
|
if dev_info is None:
|
|
115
115
|
devices_list = self.api.get_devices_list()
|
|
116
|
-
matches = [device for device in devices_list
|
|
116
|
+
matches = [device for device in devices_list if device['name'] == dev_name]
|
|
117
117
|
if not matches:
|
|
118
|
-
raise ValueError(f"
|
|
118
|
+
raise ValueError(f"未找到设备 {dev_name}")
|
|
119
119
|
elif len(matches) > 1:
|
|
120
|
-
raise ValueError(f"
|
|
120
|
+
raise ValueError(f"找到多个名为 {dev_name} 的设备")
|
|
121
121
|
else:
|
|
122
122
|
dev_info = get_device_info(matches[0]['model'])
|
|
123
123
|
did = matches[0]['did']
|
|
@@ -152,14 +152,14 @@ class mijiaDevice(object):
|
|
|
152
152
|
f"Properties:\n{prop_list_str if prop_list_str else 'No properties available'}\n"
|
|
153
153
|
f"Actions:\n{action_list_str if action_list_str else 'No actions available'}")
|
|
154
154
|
|
|
155
|
-
def set(self, name: str,
|
|
155
|
+
def set(self, name: str, value: Union[bool, int, float, str], did: Optional[str] = None) -> bool:
|
|
156
156
|
"""
|
|
157
157
|
设置设备的属性值。
|
|
158
158
|
|
|
159
159
|
Args:
|
|
160
160
|
name (str): 属性名称。
|
|
161
|
-
did (str): 设备ID。
|
|
162
161
|
value (Union[bool, int, float, str]): 属性值。
|
|
162
|
+
did (str, optional): 设备ID。如未指定,则使用实例化时的did。默认为None。
|
|
163
163
|
|
|
164
164
|
Returns:
|
|
165
165
|
bool: 执行结果(True/False)。
|
|
@@ -168,14 +168,18 @@ class mijiaDevice(object):
|
|
|
168
168
|
ValueError: 如果属性不存在、属性为只读或值无效。
|
|
169
169
|
RuntimeError: 如果设置属性失败。
|
|
170
170
|
"""
|
|
171
|
+
if did is None:
|
|
172
|
+
did = self.did
|
|
173
|
+
if did is None:
|
|
174
|
+
raise ValueError('请指定设备ID (did)')
|
|
171
175
|
if name not in self.prop_list:
|
|
172
|
-
raise ValueError(f'
|
|
176
|
+
raise ValueError(f'不支持的属性: {name}, 可用属性: {list(self.prop_list.keys())}')
|
|
173
177
|
prop = self.prop_list[name]
|
|
174
178
|
if 'w' not in prop.rw:
|
|
175
|
-
raise ValueError(f'
|
|
179
|
+
raise ValueError(f'属性 {name} 不可写入')
|
|
176
180
|
if prop.value_list:
|
|
177
181
|
if value not in [item['value'] for item in prop.value_list]:
|
|
178
|
-
raise ValueError(f'
|
|
182
|
+
raise ValueError(f'无效值: {value}, 请使用 {prop.value_list}')
|
|
179
183
|
if prop.type == 'bool':
|
|
180
184
|
if isinstance(value, str):
|
|
181
185
|
if value.lower() == 'true':
|
|
@@ -185,75 +189,53 @@ class mijiaDevice(object):
|
|
|
185
189
|
elif value in ['0', '1']:
|
|
186
190
|
value = bool(int(value))
|
|
187
191
|
else:
|
|
188
|
-
raise ValueError(f'
|
|
192
|
+
raise ValueError(f'无效布尔值: {value}')
|
|
189
193
|
elif isinstance(value, int):
|
|
190
194
|
if value == 0:
|
|
191
195
|
value = False
|
|
192
196
|
elif value == 1:
|
|
193
197
|
value = True
|
|
194
198
|
else:
|
|
195
|
-
raise ValueError(f'
|
|
199
|
+
raise ValueError(f'无效布尔值: {value}')
|
|
196
200
|
elif not isinstance(value, bool):
|
|
197
|
-
raise ValueError(f'
|
|
201
|
+
raise ValueError(f'无效布尔值: {value}')
|
|
198
202
|
elif prop.type in ['int', 'uint']:
|
|
199
203
|
value = int(value)
|
|
200
204
|
if prop.range:
|
|
201
205
|
if value < prop.range[0] or value > prop.range[1]:
|
|
202
|
-
raise ValueError(f'
|
|
206
|
+
raise ValueError(f'{value} 超出数值范围, 应该在 {prop.range[:2]} 之间')
|
|
203
207
|
if len(prop.range) >= 3 and prop.range[2] != 1:
|
|
204
208
|
if (value - prop.range[0]) % prop.range[2] != 0:
|
|
205
209
|
raise ValueError(
|
|
206
|
-
f'
|
|
210
|
+
f'无效的值: {value}, 应该在范围 {prop.range[:2]} 内且步长为 {prop.range[2]}')
|
|
207
211
|
elif prop.type == 'float':
|
|
208
212
|
value = float(value)
|
|
209
213
|
if prop.range:
|
|
210
214
|
if value < prop.range[0] or value > prop.range[1]:
|
|
211
|
-
raise ValueError(f'
|
|
215
|
+
raise ValueError(f'{value} 超出数值范围, 应该在 {prop.range[:2]} 之间')
|
|
212
216
|
if len(prop.range) >= 3 and isinstance(prop.range[2], int):
|
|
213
217
|
if int(value - prop.range[0]) % prop.range[2] != 0:
|
|
214
218
|
raise ValueError(
|
|
215
|
-
f'
|
|
219
|
+
f'无效的值: {value}, 应该在范围 {prop.range[:2]} 内且步长为 {prop.range[2]}')
|
|
216
220
|
elif prop.type == 'string':
|
|
217
221
|
if not isinstance(value, str):
|
|
218
|
-
raise ValueError(f'
|
|
222
|
+
raise ValueError(f'无效字符串值: {value}')
|
|
219
223
|
else:
|
|
220
|
-
raise ValueError(f'
|
|
224
|
+
raise ValueError(f'不支持的类型: {prop.type}, 可用类型: bool, int, uint, float, string')
|
|
221
225
|
method = prop.method.copy()
|
|
222
226
|
method['did'] = did
|
|
223
227
|
method['value'] = value
|
|
224
228
|
result = self.api.set_devices_prop([method])[0]
|
|
225
229
|
if result['code'] != 0:
|
|
226
230
|
raise RuntimeError(
|
|
227
|
-
f"
|
|
228
|
-
f"
|
|
229
|
-
f"
|
|
231
|
+
f"设置属性 {name} 失败, "
|
|
232
|
+
f"错误码: {result['code']}, "
|
|
233
|
+
f"错误信息: {ERROR_CODE.get(str(result['code']), '未知错误')}"
|
|
230
234
|
)
|
|
231
235
|
sleep(self.sleep_time)
|
|
232
|
-
logger.debug(f"
|
|
236
|
+
logger.debug(f"设置属性: {self.name} -> {name}, 值: {value}, 结果: {result}")
|
|
233
237
|
return result['code'] == 0
|
|
234
238
|
|
|
235
|
-
def set_v2(self, name: str, value: Union[bool, int, float, str], did: Optional[str] = None) -> bool:
|
|
236
|
-
"""
|
|
237
|
-
设置设备的属性值(v2版本,需在实例化时指定did或在调用时提供)。
|
|
238
|
-
|
|
239
|
-
Args:
|
|
240
|
-
name (str): 属性名称。
|
|
241
|
-
value (Union[bool, int, float, str]): 属性值。
|
|
242
|
-
did (str, optional): 设备ID。如未指定,则使用实例化时的did。默认为None。
|
|
243
|
-
|
|
244
|
-
Returns:
|
|
245
|
-
bool: 执行结果(True/False)。
|
|
246
|
-
|
|
247
|
-
Raises:
|
|
248
|
-
ValueError: 如果未指定设备ID。
|
|
249
|
-
"""
|
|
250
|
-
if did is not None:
|
|
251
|
-
return self.set(name, did, value)
|
|
252
|
-
elif self.did is not None:
|
|
253
|
-
return self.set(name, self.did, value)
|
|
254
|
-
else:
|
|
255
|
-
raise ValueError('Please specify the did')
|
|
256
|
-
|
|
257
239
|
def get(self, name: str, did: Optional[str] = None) -> Union[bool, int, float, str]:
|
|
258
240
|
"""
|
|
259
241
|
获取设备的属性值。
|
|
@@ -272,23 +254,23 @@ class mijiaDevice(object):
|
|
|
272
254
|
if did is None:
|
|
273
255
|
did = self.did
|
|
274
256
|
if did is None:
|
|
275
|
-
raise ValueError('
|
|
257
|
+
raise ValueError('请指定设备ID (did)')
|
|
276
258
|
if name not in self.prop_list:
|
|
277
|
-
raise ValueError(f'
|
|
259
|
+
raise ValueError(f'不支持的属性: {name}, 可用属性: {list(self.prop_list.keys())}')
|
|
278
260
|
prop = self.prop_list[name]
|
|
279
261
|
if 'r' not in prop.rw:
|
|
280
|
-
raise ValueError(f'
|
|
262
|
+
raise ValueError(f'属性 {name} 不可读取')
|
|
281
263
|
method = prop.method.copy()
|
|
282
264
|
method['did'] = did
|
|
283
265
|
result = self.api.get_devices_prop([method])[0]
|
|
284
266
|
if result['code'] != 0:
|
|
285
267
|
raise RuntimeError(
|
|
286
|
-
f"
|
|
287
|
-
f"
|
|
288
|
-
f"
|
|
268
|
+
f"获取属性 {name} 失败, "
|
|
269
|
+
f"错误码: {result['code']}, "
|
|
270
|
+
f"错误信息: {ERROR_CODE.get(str(result['code']), '未知错误')}"
|
|
289
271
|
)
|
|
290
272
|
sleep(self.sleep_time)
|
|
291
|
-
logger.debug(f"
|
|
273
|
+
logger.debug(f"获取属性: {self.name} -> {name}, 结果: {result}")
|
|
292
274
|
return result['value']
|
|
293
275
|
|
|
294
276
|
def __setattr__(self, name: str, value: Union[bool, int, float, str]) -> None:
|
|
@@ -303,8 +285,8 @@ class mijiaDevice(object):
|
|
|
303
285
|
RuntimeError: 如果设置属性失败。
|
|
304
286
|
"""
|
|
305
287
|
if 'prop_list' in self.__dict__ and name in self.prop_list:
|
|
306
|
-
if not self.
|
|
307
|
-
raise RuntimeError(f'
|
|
288
|
+
if not self.set(name, value):
|
|
289
|
+
raise RuntimeError(f'设置属性 {name} 失败')
|
|
308
290
|
else:
|
|
309
291
|
super().__setattr__(name, value)
|
|
310
292
|
|
|
@@ -349,9 +331,9 @@ class mijiaDevice(object):
|
|
|
349
331
|
if did is None:
|
|
350
332
|
did = self.did
|
|
351
333
|
if did is None:
|
|
352
|
-
raise ValueError('
|
|
334
|
+
raise ValueError('请指定设备ID (did)')
|
|
353
335
|
if name not in self.action_list:
|
|
354
|
-
raise ValueError(f'
|
|
336
|
+
raise ValueError(f'不支持的动作: {name}, 可用动作: {list(self.action_list.keys())}')
|
|
355
337
|
act = self.action_list[name]
|
|
356
338
|
method = act.method.copy()
|
|
357
339
|
method['did'] = did
|
|
@@ -362,55 +344,20 @@ class mijiaDevice(object):
|
|
|
362
344
|
if k.startswith("_"):
|
|
363
345
|
k = k[1:]
|
|
364
346
|
if k in method:
|
|
365
|
-
raise ValueError(f'
|
|
347
|
+
raise ValueError(f'无效的参数: {k}. 请勿使用以下参数 ({", ".join(method.keys())})')
|
|
366
348
|
method[k] = v
|
|
367
349
|
result = self.api.run_action(method)
|
|
368
350
|
if result['code'] != 0:
|
|
369
351
|
raise RuntimeError(
|
|
370
|
-
f"
|
|
371
|
-
f"
|
|
372
|
-
f"
|
|
352
|
+
f"执行动作 {name} 失败, "
|
|
353
|
+
f"错误码: {result['code']}, "
|
|
354
|
+
f"错误信息: {ERROR_CODE.get(str(result['code']), '未知错误')}"
|
|
373
355
|
)
|
|
374
356
|
sleep(self.sleep_time)
|
|
375
|
-
logger.debug(f"
|
|
357
|
+
logger.debug(f"执行动作: {self.name} -> {name}, 结果: {result}")
|
|
376
358
|
return result['code'] == 0
|
|
377
359
|
|
|
378
360
|
|
|
379
|
-
class mijiaDevices(mijiaDevice):
|
|
380
|
-
def __init__(
|
|
381
|
-
self,
|
|
382
|
-
api: mijiaAPI,
|
|
383
|
-
dev_info: Optional[dict] = None,
|
|
384
|
-
dev_name: Optional[str] = None,
|
|
385
|
-
did: Optional[str] = None,
|
|
386
|
-
sleep_time: Optional[Union[int, float]] = 0.5
|
|
387
|
-
):
|
|
388
|
-
"""
|
|
389
|
-
初始化设备对象。
|
|
390
|
-
|
|
391
|
-
如果未提供设备信息,则根据设备名称获取设备信息。如果两者均未提供,则抛出异常。
|
|
392
|
-
如果同时提供了设备信息和设备名称,则以设备信息为准。
|
|
393
|
-
|
|
394
|
-
Args:
|
|
395
|
-
api (mijiaAPI): 米家API对象。
|
|
396
|
-
dev_info (dict, optional): 设备信息字典,从get_device_info获取。默认为None。
|
|
397
|
-
dev_name (str, optional): 设备名称,从get_devices_list获取。默认为None。
|
|
398
|
-
did (str, optional): 设备ID,如未指定,则需要在调用get/set时指定。默认为None。
|
|
399
|
-
sleep_time ([int, float], optional): 调用设备属性的间隔时间。默认为0.5秒。
|
|
400
|
-
|
|
401
|
-
Raises:
|
|
402
|
-
RuntimeError: 如果dev_info和dev_name都未提供。
|
|
403
|
-
ValueError: 如果找不到指定设备或找到多个同名设备。
|
|
404
|
-
|
|
405
|
-
Note:
|
|
406
|
-
- 如果同时提供了dev_info和dev_name,则以dev_info为准。
|
|
407
|
-
- 如果只提供了dev_name,则根据名称自动获取设备信息。
|
|
408
|
-
- 如果只提供了dev_info,则直接使用该信息。
|
|
409
|
-
"""
|
|
410
|
-
super().__init__(api, dev_info, dev_name, did, sleep_time)
|
|
411
|
-
logger.warning("`mijiaDevices` will be deprecated in future versions, use `mijiaDevice` instead.")
|
|
412
|
-
|
|
413
|
-
|
|
414
361
|
def get_device_info(device_model: str, cache_path: Optional[str] = os.path.join(os.path.expanduser("~"), ".config/mijia-api")) -> dict:
|
|
415
362
|
"""
|
|
416
363
|
获取设备信息,用于初始化mijiaDevice对象。
|
|
@@ -432,10 +379,10 @@ def get_device_info(device_model: str, cache_path: Optional[str] = os.path.join(
|
|
|
432
379
|
return json.load(f)
|
|
433
380
|
response = requests.get(deviceURL + device_model)
|
|
434
381
|
if response.status_code != 200:
|
|
435
|
-
raise RuntimeError(f'
|
|
382
|
+
raise RuntimeError(f'获取设备信息失败')
|
|
436
383
|
content = re.search(r'data-page="(.*?)">', response.text)
|
|
437
384
|
if content is None:
|
|
438
|
-
raise RuntimeError(f'
|
|
385
|
+
raise RuntimeError(f'获取设备信息失败')
|
|
439
386
|
content = content.group(1)
|
|
440
387
|
content = json.loads(content.replace('"', '"'))
|
|
441
388
|
|
|
@@ -13,8 +13,7 @@ import requests
|
|
|
13
13
|
from qrcode import QRCode
|
|
14
14
|
|
|
15
15
|
from .logger import get_logger
|
|
16
|
-
from .
|
|
17
|
-
from .utils import defaultUA
|
|
16
|
+
from .consts import msgURL, loginURL, qrURL, accountURL, defaultUA
|
|
18
17
|
|
|
19
18
|
logger = get_logger(__name__)
|
|
20
19
|
|
|
@@ -65,7 +64,7 @@ class mijiaLogin(object):
|
|
|
65
64
|
"""
|
|
66
65
|
ret = self.session.get(msgURL)
|
|
67
66
|
if ret.status_code != 200:
|
|
68
|
-
raise LoginError(ret.status_code, f'
|
|
67
|
+
raise LoginError(ret.status_code, f'获取索引页失败, {ret.text}')
|
|
69
68
|
ret_data = json.loads(ret.text[11:])
|
|
70
69
|
data = {'deviceId': self.deviceId}
|
|
71
70
|
data.update({
|
|
@@ -90,7 +89,7 @@ class mijiaLogin(object):
|
|
|
90
89
|
try:
|
|
91
90
|
ret = self.session.get(accountURL + str(user_id))
|
|
92
91
|
if ret.status_code != 200:
|
|
93
|
-
raise LoginError(ret.status_code, f'
|
|
92
|
+
raise LoginError(ret.status_code, f'获取账户页面失败, {ret.text}')
|
|
94
93
|
data = json.loads(ret.text[11:])['data']
|
|
95
94
|
except (KeyError, json.JSONDecodeError) as e:
|
|
96
95
|
data = {}
|
|
@@ -117,11 +116,13 @@ class mijiaLogin(object):
|
|
|
117
116
|
]
|
|
118
117
|
|
|
119
118
|
if not gmt_time_keys:
|
|
120
|
-
raise LoginError(-1, '
|
|
119
|
+
raise LoginError(-1, '在cookie中未找到GMT时间键')
|
|
121
120
|
parsed_times = [datetime.strptime(k, '%d-%b-%Y %H:%M:%S GMT') for k in gmt_time_keys]
|
|
122
121
|
latest_utc_time = max(parsed_times)
|
|
123
122
|
china_time = latest_utc_time + timedelta(hours=8)
|
|
124
123
|
|
|
124
|
+
# [FIXME] 实测此处的过期时间并不准确,实际过期时间可能大于此处获取的时间
|
|
125
|
+
# cookie 中唯一用到的 serviceToken 并无过期时间
|
|
125
126
|
return china_time
|
|
126
127
|
|
|
127
128
|
def _save_auth(self) -> None:
|
|
@@ -134,14 +135,14 @@ class mijiaLogin(object):
|
|
|
134
135
|
if not os.path.isabs(self.save_path):
|
|
135
136
|
self.save_path = os.path.abspath(self.save_path)
|
|
136
137
|
if os.path.exists(self.save_path) and not os.path.isfile(self.save_path):
|
|
137
|
-
raise ValueError(f'
|
|
138
|
+
raise ValueError(f'[{self.save_path}] 不是文件')
|
|
138
139
|
if not os.path.exists(os.path.dirname(self.save_path)):
|
|
139
140
|
os.makedirs(os.path.dirname(self.save_path))
|
|
140
141
|
with open(self.save_path, 'w') as f:
|
|
141
142
|
json.dump(self.auth_data, f, indent=2)
|
|
142
|
-
logger.info(f'
|
|
143
|
+
logger.info(f'认证文件已保存到 [{self.save_path}]')
|
|
143
144
|
else:
|
|
144
|
-
logger.info('
|
|
145
|
+
logger.info('认证文件未保存')
|
|
145
146
|
|
|
146
147
|
def login(self, username: str, password: str) -> dict:
|
|
147
148
|
"""
|
|
@@ -157,7 +158,7 @@ class mijiaLogin(object):
|
|
|
157
158
|
Raises:
|
|
158
159
|
LoginError: 登录失败时抛出。
|
|
159
160
|
"""
|
|
160
|
-
logger.warning('
|
|
161
|
+
logger.warning('使用账号密码登录很可能需要验证码。请尝试使用 `QRlogin` 方法。')
|
|
161
162
|
data = self._get_index()
|
|
162
163
|
post_data = {
|
|
163
164
|
'qs': data['qs'],
|
|
@@ -170,17 +171,17 @@ class mijiaLogin(object):
|
|
|
170
171
|
}
|
|
171
172
|
ret = self.session.post(loginURL, data=post_data)
|
|
172
173
|
if ret.status_code != 200:
|
|
173
|
-
raise LoginError(ret.status_code, f'
|
|
174
|
+
raise LoginError(ret.status_code, f'登录页面提交失败, {ret.text}')
|
|
174
175
|
ret_data = json.loads(ret.text[11:])
|
|
175
176
|
if ret_data['code'] != 0:
|
|
176
177
|
raise LoginError(ret_data['code'], ret_data['desc'])
|
|
177
178
|
if 'location' not in ret_data:
|
|
178
|
-
raise LoginError(-1, '
|
|
179
|
+
raise LoginError(-1, '获取跳转位置失败')
|
|
179
180
|
if 'notificationUrl' in ret_data:
|
|
180
|
-
raise LoginError(-1, '
|
|
181
|
+
raise LoginError(-1, '需要验证码,请尝试使用 `QRlogin` 方法')
|
|
181
182
|
ret = self.session.get(ret_data['location'])
|
|
182
183
|
if ret.status_code != 200:
|
|
183
|
-
raise LoginError(ret.status_code, f'
|
|
184
|
+
raise LoginError(ret.status_code, f'获取跳转位置失败, {ret.text}')
|
|
184
185
|
cookies = self.session.cookies.get_dict()
|
|
185
186
|
|
|
186
187
|
self.auth_data = {
|
|
@@ -188,6 +189,7 @@ class mijiaLogin(object):
|
|
|
188
189
|
'ssecurity': ret_data['ssecurity'],
|
|
189
190
|
'deviceId': data['deviceId'],
|
|
190
191
|
'serviceToken': cookies['serviceToken'],
|
|
192
|
+
'cUserId': cookies['cUserId'],
|
|
191
193
|
'expireTime': self._extract_latest_gmt_datetime(cookies).strftime('%Y-%m-%d %H:%M:%S'),
|
|
192
194
|
'account_info': self._get_account_info(ret_data['userId'])
|
|
193
195
|
}
|
|
@@ -204,7 +206,7 @@ class mijiaLogin(object):
|
|
|
204
206
|
loginurl (str): 包含登录信息的URL。
|
|
205
207
|
box_size (int, optional): 二维码大小。默认为10。
|
|
206
208
|
"""
|
|
207
|
-
logger.info('
|
|
209
|
+
logger.info('请使用米家APP扫描下方二维码')
|
|
208
210
|
qr = QRCode(border=1, box_size=box_size)
|
|
209
211
|
qr.add_data(loginurl)
|
|
210
212
|
qr.make_image().save('qr.png')
|
|
@@ -212,10 +214,10 @@ class mijiaLogin(object):
|
|
|
212
214
|
qr.print_ascii(invert=True, tty=True)
|
|
213
215
|
except OSError:
|
|
214
216
|
qr.print_ascii(invert=True, tty=False)
|
|
215
|
-
logger.info('
|
|
216
|
-
'
|
|
217
|
-
'
|
|
218
|
-
'
|
|
217
|
+
logger.info('如果无法扫描二维码,'
|
|
218
|
+
'请更改终端字体,'
|
|
219
|
+
'如"Maple Mono"、"Fira Code"等。\n'
|
|
220
|
+
'或者直接使用当前目录下的qr.png文件。')
|
|
219
221
|
|
|
220
222
|
def QRlogin(self) -> dict:
|
|
221
223
|
"""
|
|
@@ -248,7 +250,7 @@ class mijiaLogin(object):
|
|
|
248
250
|
url = qrURL + '?' + parse.urlencode(params)
|
|
249
251
|
ret = self.session.get(url)
|
|
250
252
|
if ret.status_code != 200:
|
|
251
|
-
raise LoginError(ret.status_code, f'
|
|
253
|
+
raise LoginError(ret.status_code, f'获取二维码URL失败, {ret.text}')
|
|
252
254
|
ret_data = json.loads(ret.text[11:])
|
|
253
255
|
if ret_data['code'] != 0:
|
|
254
256
|
raise LoginError(ret_data['code'], ret_data['desc'])
|
|
@@ -257,15 +259,15 @@ class mijiaLogin(object):
|
|
|
257
259
|
try:
|
|
258
260
|
ret = self.session.get(ret_data['lp'], timeout=60, headers={'Connection': 'keep-alive'})
|
|
259
261
|
except requests.exceptions.Timeout:
|
|
260
|
-
raise LoginError(-1, '
|
|
262
|
+
raise LoginError(-1, '超时,请重试')
|
|
261
263
|
if ret.status_code != 200:
|
|
262
|
-
raise LoginError(ret.status_code, f'
|
|
264
|
+
raise LoginError(ret.status_code, f'等待登录失败, {ret.text}')
|
|
263
265
|
ret_data = json.loads(ret.text[11:])
|
|
264
266
|
if ret_data['code'] != 0:
|
|
265
267
|
raise LoginError(ret_data['code'], ret_data['desc'])
|
|
266
268
|
ret = self.session.get(ret_data['location'])
|
|
267
269
|
if ret.status_code != 200:
|
|
268
|
-
raise LoginError(ret.status_code, f'
|
|
270
|
+
raise LoginError(ret.status_code, f'获取跳转位置失败, {ret.text}')
|
|
269
271
|
cookies = self.session.cookies.get_dict()
|
|
270
272
|
|
|
271
273
|
self.auth_data = {
|
|
@@ -273,6 +275,7 @@ class mijiaLogin(object):
|
|
|
273
275
|
'ssecurity': ret_data['ssecurity'],
|
|
274
276
|
'deviceId': data['deviceId'],
|
|
275
277
|
'serviceToken': cookies['serviceToken'],
|
|
278
|
+
'cUserId': cookies['cUserId'],
|
|
276
279
|
'expireTime': self._extract_latest_gmt_datetime(cookies).strftime('%Y-%m-%d %H:%M:%S'),
|
|
277
280
|
'account_info': self._get_account_info(ret_data['userId'])
|
|
278
281
|
}
|
|
@@ -6,9 +6,8 @@ import string
|
|
|
6
6
|
|
|
7
7
|
import requests
|
|
8
8
|
|
|
9
|
-
from .
|
|
9
|
+
from .consts import apiURL
|
|
10
10
|
|
|
11
|
-
defaultUA = 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36 Edg/126.0.0.0'
|
|
12
11
|
|
|
13
12
|
class PostDataError(Exception):
|
|
14
13
|
def __init__(self, code: int, message: str):
|
|
@@ -38,6 +37,6 @@ def post_data(session: requests.Session, ssecurity: str, uri: str, data: dict) -
|
|
|
38
37
|
post_data = {'_nonce': nonce, 'data': data, 'signature': signature}
|
|
39
38
|
ret = session.post(apiURL + uri, data=post_data)
|
|
40
39
|
if ret.status_code != 200:
|
|
41
|
-
raise PostDataError(ret.status_code, f'
|
|
40
|
+
raise PostDataError(ret.status_code, f'发送数据失败, {ret.text}')
|
|
42
41
|
ret_data = ret.json()
|
|
43
42
|
return ret_data
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mijiaAPI"
|
|
3
|
-
version = "
|
|
3
|
+
version = "2.0.1"
|
|
4
4
|
description = "A Python API for Xiaomi Mijia"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.9,<4.0"
|
|
@@ -15,7 +15,7 @@ mijiaAPI = "mijiaAPI.__main__:cli"
|
|
|
15
15
|
|
|
16
16
|
[tool.poetry]
|
|
17
17
|
name = "mijiaAPI"
|
|
18
|
-
version = "
|
|
18
|
+
version = "2.0.1"
|
|
19
19
|
description = "A Python API for Xiaomi Mijia"
|
|
20
20
|
authors = ["Do1e <dpj.email@qq.com>"]
|
|
21
21
|
license = "GPLv3"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|