mijiaAPI 2.0.0__tar.gz → 2.0.2__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-2.0.0 → mijiaapi-2.0.2}/PKG-INFO +4 -19
- {mijiaapi-2.0.0 → mijiaapi-2.0.2}/README.md +0 -17
- {mijiaapi-2.0.0 → mijiaapi-2.0.2}/mijiaAPI/__main__.py +24 -24
- {mijiaapi-2.0.0 → mijiaapi-2.0.2}/mijiaAPI/apis.py +24 -15
- mijiaapi-2.0.0/mijiaAPI/urls.py → mijiaapi-2.0.2/mijiaAPI/consts.py +2 -0
- {mijiaapi-2.0.0 → mijiaapi-2.0.2}/mijiaAPI/devices.py +42 -42
- {mijiaapi-2.0.0 → mijiaapi-2.0.2}/mijiaAPI/login.py +21 -22
- {mijiaapi-2.0.0 → mijiaapi-2.0.2}/mijiaAPI/utils.py +2 -3
- {mijiaapi-2.0.0 → mijiaapi-2.0.2}/pyproject.toml +2 -2
- {mijiaapi-2.0.0 → mijiaapi-2.0.2}/LICENSE +0 -0
- {mijiaapi-2.0.0 → mijiaapi-2.0.2}/mijiaAPI/__init__.py +0 -0
- {mijiaapi-2.0.0 → mijiaapi-2.0.2}/mijiaAPI/code.py +0 -0
- {mijiaapi-2.0.0 → mijiaapi-2.0.2}/mijiaAPI/logger.py +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: mijiaAPI
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: A Python API for Xiaomi Mijia
|
|
5
5
|
License: GPLv3
|
|
6
|
+
License-File: LICENSE
|
|
6
7
|
Author: Do1e
|
|
7
8
|
Author-email: dpj.email@qq.com
|
|
8
9
|
Requires-Python: >=3.9,<4.0
|
|
@@ -15,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
15
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
16
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
20
|
Requires-Dist: pillow (>=11.0.0,<12.0.0)
|
|
19
21
|
Requires-Dist: qrcode (>=8.0,<9.0)
|
|
20
22
|
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
|
@@ -264,23 +266,6 @@ mijiaAPI --run 明天天气如何
|
|
|
264
266
|
mijiaAPI --run 打开台灯并将亮度调至最大 --quiet
|
|
265
267
|
```
|
|
266
268
|
|
|
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
|
-
|
|
284
269
|
## 致谢
|
|
285
270
|
|
|
286
271
|
* [janzlan/mijia-api](https://gitee.com/janzlan/mijia-api/tree/master)
|
|
@@ -240,23 +240,6 @@ mijiaAPI --run 明天天气如何
|
|
|
240
240
|
mijiaAPI --run 打开台灯并将亮度调至最大 --quiet
|
|
241
241
|
```
|
|
242
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
|
-
|
|
260
243
|
## 致谢
|
|
261
244
|
|
|
262
245
|
* [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()
|
|
@@ -147,7 +147,7 @@ def init_api(auth_path: str) -> mijiaAPI:
|
|
|
147
147
|
def get_devices_list(api: mijiaAPI, verbose: bool = True) -> dict:
|
|
148
148
|
devices = api.get_devices_list()
|
|
149
149
|
if verbose:
|
|
150
|
-
print("
|
|
150
|
+
print("设备列表:")
|
|
151
151
|
for device in devices:
|
|
152
152
|
print(f" - {device['name']}\n"
|
|
153
153
|
f" did: {device['did']}\n"
|
|
@@ -162,14 +162,14 @@ def get_homes_list(api: mijiaAPI, verbose: bool = True, device_mapping: Optional
|
|
|
162
162
|
device_mapping = get_devices_list(api, verbose=False)
|
|
163
163
|
homes = api.get_homes_list()
|
|
164
164
|
if verbose:
|
|
165
|
-
print("
|
|
165
|
+
print("家庭列表:")
|
|
166
166
|
for home in homes:
|
|
167
167
|
print(f" - {home['name']}\n"
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
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( " 房间列表:")
|
|
173
173
|
for room in home['roomlist']:
|
|
174
174
|
devices_name = []
|
|
175
175
|
if room['dids']:
|
|
@@ -180,9 +180,9 @@ def get_homes_list(api: mijiaAPI, verbose: bool = True, device_mapping: Optional
|
|
|
180
180
|
devices_name.append(did)
|
|
181
181
|
dids = ', '.join(devices_name)
|
|
182
182
|
print(f" - {room['name']}\n"
|
|
183
|
-
f"
|
|
184
|
-
f"
|
|
185
|
-
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']))}")
|
|
186
186
|
home_mapping = {home['id']: home for home in homes}
|
|
187
187
|
return home_mapping
|
|
188
188
|
|
|
@@ -193,11 +193,11 @@ def get_scenes_list(api: mijiaAPI, verbose: bool = True, home_mapping: Optional[
|
|
|
193
193
|
for home_id, home in home_mapping.items():
|
|
194
194
|
scenes = api.get_scenes_list(home_id)
|
|
195
195
|
if scenes and verbose:
|
|
196
|
-
print(f"
|
|
196
|
+
print(f"{home['name']} ({home_id}) 中的场景:")
|
|
197
197
|
for scene in scenes:
|
|
198
198
|
print(f" - {scene['name']}\n"
|
|
199
|
-
f"
|
|
200
|
-
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"
|
|
201
201
|
f" update time: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(int(scene['update_time'])))}")
|
|
202
202
|
scene_mapping.update({scene['scene_id']: scene for scene in scenes})
|
|
203
203
|
return scene_mapping
|
|
@@ -207,11 +207,11 @@ def get_consumable_items(api: mijiaAPI, home_mapping: Optional[dict] = None):
|
|
|
207
207
|
home_mapping = get_homes_list(api, verbose=False)
|
|
208
208
|
for home_id, home in home_mapping.items():
|
|
209
209
|
items = api.get_consumable_items(home_id, home['uid'])
|
|
210
|
-
print(f"
|
|
210
|
+
print(f"{home['name']} ({home_id}) 中的耗材:")
|
|
211
211
|
for item in items:
|
|
212
212
|
for consumes_data in item['consumes_data']:
|
|
213
|
-
print(f" - {consumes_data['details'][0]['description']}
|
|
214
|
-
|
|
213
|
+
print(f" - {consumes_data['details'][0]['description']} 在 {consumes_data['name']}({consumes_data['did']})\n"
|
|
214
|
+
f" 值: {consumes_data['details'][0]['value']}")
|
|
215
215
|
|
|
216
216
|
def run_scene(api: mijiaAPI, scene_id: str, scene_mapping: Optional[dict] = None) -> bool:
|
|
217
217
|
if scene_mapping is None:
|
|
@@ -225,15 +225,15 @@ def run_scene(api: mijiaAPI, scene_id: str, scene_mapping: Optional[dict] = None
|
|
|
225
225
|
found = True
|
|
226
226
|
break
|
|
227
227
|
if not found:
|
|
228
|
-
print(f"
|
|
228
|
+
print(f"场景 {scene_name_to_find} 未找到")
|
|
229
229
|
return False
|
|
230
230
|
scene_name = scene_mapping[scene_id]['name']
|
|
231
231
|
ret = api.run_scene(scene_id)
|
|
232
232
|
if ret:
|
|
233
|
-
print(f"
|
|
233
|
+
print(f"场景 {scene_name}({scene_id}) 运行成功")
|
|
234
234
|
return True
|
|
235
235
|
else:
|
|
236
|
-
print(f"
|
|
236
|
+
print(f"运行场景 {scene_name}({scene_id}) 失败")
|
|
237
237
|
return False
|
|
238
238
|
|
|
239
239
|
def get(args):
|
|
@@ -241,7 +241,7 @@ def get(args):
|
|
|
241
241
|
device = mijiaDevice(api, dev_name=args.dev_name)
|
|
242
242
|
value = device.get(args.prop_name)
|
|
243
243
|
unit = device.prop_list[args.prop_name].unit
|
|
244
|
-
print(f"
|
|
244
|
+
print(f"{args.dev_name} 的 {args.prop_name} 值为 {value} {unit if unit else ''}")
|
|
245
245
|
|
|
246
246
|
def set(args):
|
|
247
247
|
api = init_api(args.auth_path)
|
|
@@ -249,9 +249,9 @@ def set(args):
|
|
|
249
249
|
ret = device.set(args.prop_name, args.value)
|
|
250
250
|
unit = device.prop_list[args.prop_name].unit
|
|
251
251
|
if ret:
|
|
252
|
-
print(f"
|
|
252
|
+
print(f"{args.dev_name} 的 {args.prop_name} 值已设置为 {args.value} {unit if unit else ''}")
|
|
253
253
|
else:
|
|
254
|
-
print(f"
|
|
254
|
+
print(f"设置 {args.dev_name} 的 {args.prop_name} 值为 {args.value} 失败")
|
|
255
255
|
|
|
256
256
|
|
|
257
257
|
def main(args):
|
|
@@ -295,7 +295,7 @@ def main(args):
|
|
|
295
295
|
wifispeaker = mijiaDevice(api, dev_name=device['name'])
|
|
296
296
|
break
|
|
297
297
|
if wifispeaker is None:
|
|
298
|
-
raise ValueError("
|
|
298
|
+
raise ValueError("未找到小爱音箱设备")
|
|
299
299
|
else:
|
|
300
300
|
wifispeaker = mijiaDevice(api, dev_name=args.wifispeaker_name)
|
|
301
301
|
wifispeaker.run_action('execute-text-directive', _in=[args.run, args.quiet])
|
|
@@ -4,7 +4,8 @@ from typing import Union, Optional
|
|
|
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
|
|
@@ -64,18 +65,26 @@ class mijiaAPI(object):
|
|
|
64
65
|
home_list = self.get_homes_list()
|
|
65
66
|
devices = []
|
|
66
67
|
for home in home_list:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
79
88
|
return devices
|
|
80
89
|
|
|
81
90
|
def get_homes_list(self) -> list:
|
|
@@ -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
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']
|
|
@@ -171,15 +171,12 @@ class mijiaDevice(object):
|
|
|
171
171
|
if did is None:
|
|
172
172
|
did = self.did
|
|
173
173
|
if did is None:
|
|
174
|
-
raise ValueError('
|
|
174
|
+
raise ValueError('请指定设备ID (did)')
|
|
175
175
|
if name not in self.prop_list:
|
|
176
|
-
raise ValueError(f'
|
|
176
|
+
raise ValueError(f'不支持的属性: {name}, 可用属性: {list(self.prop_list.keys())}')
|
|
177
177
|
prop = self.prop_list[name]
|
|
178
178
|
if 'w' not in prop.rw:
|
|
179
|
-
raise ValueError(f'
|
|
180
|
-
if prop.value_list:
|
|
181
|
-
if value not in [item['value'] for item in prop.value_list]:
|
|
182
|
-
raise ValueError(f'Invalid value: {value}, should be in {prop.value_list}')
|
|
179
|
+
raise ValueError(f'属性 {name} 不可写入')
|
|
183
180
|
if prop.type == 'bool':
|
|
184
181
|
if isinstance(value, str):
|
|
185
182
|
if value.lower() == 'true':
|
|
@@ -189,51 +186,54 @@ class mijiaDevice(object):
|
|
|
189
186
|
elif value in ['0', '1']:
|
|
190
187
|
value = bool(int(value))
|
|
191
188
|
else:
|
|
192
|
-
raise ValueError(f'
|
|
189
|
+
raise ValueError(f'无效布尔值: {value}')
|
|
193
190
|
elif isinstance(value, int):
|
|
194
191
|
if value == 0:
|
|
195
192
|
value = False
|
|
196
193
|
elif value == 1:
|
|
197
194
|
value = True
|
|
198
195
|
else:
|
|
199
|
-
raise ValueError(f'
|
|
196
|
+
raise ValueError(f'无效布尔值: {value}')
|
|
200
197
|
elif not isinstance(value, bool):
|
|
201
|
-
raise ValueError(f'
|
|
198
|
+
raise ValueError(f'无效布尔值: {value}')
|
|
202
199
|
elif prop.type in ['int', 'uint']:
|
|
203
200
|
value = int(value)
|
|
204
201
|
if prop.range:
|
|
205
202
|
if value < prop.range[0] or value > prop.range[1]:
|
|
206
|
-
raise ValueError(f'
|
|
203
|
+
raise ValueError(f'{value} 超出数值范围, 应该在 {prop.range[:2]} 之间')
|
|
207
204
|
if len(prop.range) >= 3 and prop.range[2] != 1:
|
|
208
205
|
if (value - prop.range[0]) % prop.range[2] != 0:
|
|
209
206
|
raise ValueError(
|
|
210
|
-
f'
|
|
207
|
+
f'无效的值: {value}, 应该在范围 {prop.range[:2]} 内且步长为 {prop.range[2]}')
|
|
211
208
|
elif prop.type == 'float':
|
|
212
209
|
value = float(value)
|
|
213
210
|
if prop.range:
|
|
214
211
|
if value < prop.range[0] or value > prop.range[1]:
|
|
215
|
-
raise ValueError(f'
|
|
212
|
+
raise ValueError(f'{value} 超出数值范围, 应该在 {prop.range[:2]} 之间')
|
|
216
213
|
if len(prop.range) >= 3 and isinstance(prop.range[2], int):
|
|
217
214
|
if int(value - prop.range[0]) % prop.range[2] != 0:
|
|
218
215
|
raise ValueError(
|
|
219
|
-
f'
|
|
216
|
+
f'无效的值: {value}, 应该在范围 {prop.range[:2]} 内且步长为 {prop.range[2]}')
|
|
220
217
|
elif prop.type == 'string':
|
|
221
218
|
if not isinstance(value, str):
|
|
222
|
-
raise ValueError(f'
|
|
219
|
+
raise ValueError(f'无效字符串值: {value}')
|
|
223
220
|
else:
|
|
224
|
-
raise ValueError(f'
|
|
221
|
+
raise ValueError(f'不支持的类型: {prop.type}, 可用类型: bool, int, uint, float, string')
|
|
222
|
+
if prop.value_list:
|
|
223
|
+
if value not in [item['value'] for item in prop.value_list]:
|
|
224
|
+
raise ValueError(f'无效值: {value}, 请使用 {prop.value_list}')
|
|
225
225
|
method = prop.method.copy()
|
|
226
226
|
method['did'] = did
|
|
227
227
|
method['value'] = value
|
|
228
228
|
result = self.api.set_devices_prop([method])[0]
|
|
229
229
|
if result['code'] != 0:
|
|
230
230
|
raise RuntimeError(
|
|
231
|
-
f"
|
|
232
|
-
f"
|
|
233
|
-
f"
|
|
231
|
+
f"设置属性 {name} 失败, "
|
|
232
|
+
f"错误码: {result['code']}, "
|
|
233
|
+
f"错误信息: {ERROR_CODE.get(str(result['code']), '未知错误')}"
|
|
234
234
|
)
|
|
235
235
|
sleep(self.sleep_time)
|
|
236
|
-
logger.debug(f"
|
|
236
|
+
logger.debug(f"设置属性: {self.name} -> {name}, 值: {value}, 结果: {result}")
|
|
237
237
|
return result['code'] == 0
|
|
238
238
|
|
|
239
239
|
def get(self, name: str, did: Optional[str] = None) -> Union[bool, int, float, str]:
|
|
@@ -254,23 +254,23 @@ class mijiaDevice(object):
|
|
|
254
254
|
if did is None:
|
|
255
255
|
did = self.did
|
|
256
256
|
if did is None:
|
|
257
|
-
raise ValueError('
|
|
257
|
+
raise ValueError('请指定设备ID (did)')
|
|
258
258
|
if name not in self.prop_list:
|
|
259
|
-
raise ValueError(f'
|
|
259
|
+
raise ValueError(f'不支持的属性: {name}, 可用属性: {list(self.prop_list.keys())}')
|
|
260
260
|
prop = self.prop_list[name]
|
|
261
261
|
if 'r' not in prop.rw:
|
|
262
|
-
raise ValueError(f'
|
|
262
|
+
raise ValueError(f'属性 {name} 不可读取')
|
|
263
263
|
method = prop.method.copy()
|
|
264
264
|
method['did'] = did
|
|
265
265
|
result = self.api.get_devices_prop([method])[0]
|
|
266
266
|
if result['code'] != 0:
|
|
267
267
|
raise RuntimeError(
|
|
268
|
-
f"
|
|
269
|
-
f"
|
|
270
|
-
f"
|
|
268
|
+
f"获取属性 {name} 失败, "
|
|
269
|
+
f"错误码: {result['code']}, "
|
|
270
|
+
f"错误信息: {ERROR_CODE.get(str(result['code']), '未知错误')}"
|
|
271
271
|
)
|
|
272
272
|
sleep(self.sleep_time)
|
|
273
|
-
logger.debug(f"
|
|
273
|
+
logger.debug(f"获取属性: {self.name} -> {name}, 结果: {result}")
|
|
274
274
|
return result['value']
|
|
275
275
|
|
|
276
276
|
def __setattr__(self, name: str, value: Union[bool, int, float, str]) -> None:
|
|
@@ -286,7 +286,7 @@ class mijiaDevice(object):
|
|
|
286
286
|
"""
|
|
287
287
|
if 'prop_list' in self.__dict__ and name in self.prop_list:
|
|
288
288
|
if not self.set(name, value):
|
|
289
|
-
raise RuntimeError(f'
|
|
289
|
+
raise RuntimeError(f'设置属性 {name} 失败')
|
|
290
290
|
else:
|
|
291
291
|
super().__setattr__(name, value)
|
|
292
292
|
|
|
@@ -331,9 +331,9 @@ class mijiaDevice(object):
|
|
|
331
331
|
if did is None:
|
|
332
332
|
did = self.did
|
|
333
333
|
if did is None:
|
|
334
|
-
raise ValueError('
|
|
334
|
+
raise ValueError('请指定设备ID (did)')
|
|
335
335
|
if name not in self.action_list:
|
|
336
|
-
raise ValueError(f'
|
|
336
|
+
raise ValueError(f'不支持的动作: {name}, 可用动作: {list(self.action_list.keys())}')
|
|
337
337
|
act = self.action_list[name]
|
|
338
338
|
method = act.method.copy()
|
|
339
339
|
method['did'] = did
|
|
@@ -344,17 +344,17 @@ class mijiaDevice(object):
|
|
|
344
344
|
if k.startswith("_"):
|
|
345
345
|
k = k[1:]
|
|
346
346
|
if k in method:
|
|
347
|
-
raise ValueError(f'
|
|
347
|
+
raise ValueError(f'无效的参数: {k}. 请勿使用以下参数 ({", ".join(method.keys())})')
|
|
348
348
|
method[k] = v
|
|
349
349
|
result = self.api.run_action(method)
|
|
350
350
|
if result['code'] != 0:
|
|
351
351
|
raise RuntimeError(
|
|
352
|
-
f"
|
|
353
|
-
f"
|
|
354
|
-
f"
|
|
352
|
+
f"执行动作 {name} 失败, "
|
|
353
|
+
f"错误码: {result['code']}, "
|
|
354
|
+
f"错误信息: {ERROR_CODE.get(str(result['code']), '未知错误')}"
|
|
355
355
|
)
|
|
356
356
|
sleep(self.sleep_time)
|
|
357
|
-
logger.debug(f"
|
|
357
|
+
logger.debug(f"执行动作: {self.name} -> {name}, 结果: {result}")
|
|
358
358
|
return result['code'] == 0
|
|
359
359
|
|
|
360
360
|
|
|
@@ -379,10 +379,10 @@ def get_device_info(device_model: str, cache_path: Optional[str] = os.path.join(
|
|
|
379
379
|
return json.load(f)
|
|
380
380
|
response = requests.get(deviceURL + device_model)
|
|
381
381
|
if response.status_code != 200:
|
|
382
|
-
raise RuntimeError(f'
|
|
382
|
+
raise RuntimeError(f'获取设备信息失败')
|
|
383
383
|
content = re.search(r'data-page="(.*?)">', response.text)
|
|
384
384
|
if content is None:
|
|
385
|
-
raise RuntimeError(f'
|
|
385
|
+
raise RuntimeError(f'获取设备信息失败')
|
|
386
386
|
content = content.group(1)
|
|
387
387
|
content = json.loads(content.replace('"', '"'))
|
|
388
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,7 +116,7 @@ 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)
|
|
@@ -136,14 +135,14 @@ class mijiaLogin(object):
|
|
|
136
135
|
if not os.path.isabs(self.save_path):
|
|
137
136
|
self.save_path = os.path.abspath(self.save_path)
|
|
138
137
|
if os.path.exists(self.save_path) and not os.path.isfile(self.save_path):
|
|
139
|
-
raise ValueError(f'
|
|
138
|
+
raise ValueError(f'[{self.save_path}] 不是文件')
|
|
140
139
|
if not os.path.exists(os.path.dirname(self.save_path)):
|
|
141
140
|
os.makedirs(os.path.dirname(self.save_path))
|
|
142
141
|
with open(self.save_path, 'w') as f:
|
|
143
142
|
json.dump(self.auth_data, f, indent=2)
|
|
144
|
-
logger.info(f'
|
|
143
|
+
logger.info(f'认证文件已保存到 [{self.save_path}]')
|
|
145
144
|
else:
|
|
146
|
-
logger.info('
|
|
145
|
+
logger.info('认证文件未保存')
|
|
147
146
|
|
|
148
147
|
def login(self, username: str, password: str) -> dict:
|
|
149
148
|
"""
|
|
@@ -159,7 +158,7 @@ class mijiaLogin(object):
|
|
|
159
158
|
Raises:
|
|
160
159
|
LoginError: 登录失败时抛出。
|
|
161
160
|
"""
|
|
162
|
-
logger.warning('
|
|
161
|
+
logger.warning('使用账号密码登录很可能需要验证码。请尝试使用 `QRlogin` 方法。')
|
|
163
162
|
data = self._get_index()
|
|
164
163
|
post_data = {
|
|
165
164
|
'qs': data['qs'],
|
|
@@ -172,17 +171,17 @@ class mijiaLogin(object):
|
|
|
172
171
|
}
|
|
173
172
|
ret = self.session.post(loginURL, data=post_data)
|
|
174
173
|
if ret.status_code != 200:
|
|
175
|
-
raise LoginError(ret.status_code, f'
|
|
174
|
+
raise LoginError(ret.status_code, f'登录页面提交失败, {ret.text}')
|
|
176
175
|
ret_data = json.loads(ret.text[11:])
|
|
177
176
|
if ret_data['code'] != 0:
|
|
178
177
|
raise LoginError(ret_data['code'], ret_data['desc'])
|
|
179
178
|
if 'location' not in ret_data:
|
|
180
|
-
raise LoginError(-1, '
|
|
179
|
+
raise LoginError(-1, '获取跳转位置失败')
|
|
181
180
|
if 'notificationUrl' in ret_data:
|
|
182
|
-
raise LoginError(-1, '
|
|
181
|
+
raise LoginError(-1, '需要验证码,请尝试使用 `QRlogin` 方法')
|
|
183
182
|
ret = self.session.get(ret_data['location'])
|
|
184
183
|
if ret.status_code != 200:
|
|
185
|
-
raise LoginError(ret.status_code, f'
|
|
184
|
+
raise LoginError(ret.status_code, f'获取跳转位置失败, {ret.text}')
|
|
186
185
|
cookies = self.session.cookies.get_dict()
|
|
187
186
|
|
|
188
187
|
self.auth_data = {
|
|
@@ -207,7 +206,7 @@ class mijiaLogin(object):
|
|
|
207
206
|
loginurl (str): 包含登录信息的URL。
|
|
208
207
|
box_size (int, optional): 二维码大小。默认为10。
|
|
209
208
|
"""
|
|
210
|
-
logger.info('
|
|
209
|
+
logger.info('请使用米家APP扫描下方二维码')
|
|
211
210
|
qr = QRCode(border=1, box_size=box_size)
|
|
212
211
|
qr.add_data(loginurl)
|
|
213
212
|
qr.make_image().save('qr.png')
|
|
@@ -215,10 +214,10 @@ class mijiaLogin(object):
|
|
|
215
214
|
qr.print_ascii(invert=True, tty=True)
|
|
216
215
|
except OSError:
|
|
217
216
|
qr.print_ascii(invert=True, tty=False)
|
|
218
|
-
logger.info('
|
|
219
|
-
'
|
|
220
|
-
'
|
|
221
|
-
'
|
|
217
|
+
logger.info('如果无法扫描二维码,'
|
|
218
|
+
'请更改终端字体,'
|
|
219
|
+
'如"Maple Mono"、"Fira Code"等。\n'
|
|
220
|
+
'或者直接使用当前目录下的qr.png文件。')
|
|
222
221
|
|
|
223
222
|
def QRlogin(self) -> dict:
|
|
224
223
|
"""
|
|
@@ -251,7 +250,7 @@ class mijiaLogin(object):
|
|
|
251
250
|
url = qrURL + '?' + parse.urlencode(params)
|
|
252
251
|
ret = self.session.get(url)
|
|
253
252
|
if ret.status_code != 200:
|
|
254
|
-
raise LoginError(ret.status_code, f'
|
|
253
|
+
raise LoginError(ret.status_code, f'获取二维码URL失败, {ret.text}')
|
|
255
254
|
ret_data = json.loads(ret.text[11:])
|
|
256
255
|
if ret_data['code'] != 0:
|
|
257
256
|
raise LoginError(ret_data['code'], ret_data['desc'])
|
|
@@ -260,15 +259,15 @@ class mijiaLogin(object):
|
|
|
260
259
|
try:
|
|
261
260
|
ret = self.session.get(ret_data['lp'], timeout=60, headers={'Connection': 'keep-alive'})
|
|
262
261
|
except requests.exceptions.Timeout:
|
|
263
|
-
raise LoginError(-1, '
|
|
262
|
+
raise LoginError(-1, '超时,请重试')
|
|
264
263
|
if ret.status_code != 200:
|
|
265
|
-
raise LoginError(ret.status_code, f'
|
|
264
|
+
raise LoginError(ret.status_code, f'等待登录失败, {ret.text}')
|
|
266
265
|
ret_data = json.loads(ret.text[11:])
|
|
267
266
|
if ret_data['code'] != 0:
|
|
268
267
|
raise LoginError(ret_data['code'], ret_data['desc'])
|
|
269
268
|
ret = self.session.get(ret_data['location'])
|
|
270
269
|
if ret.status_code != 200:
|
|
271
|
-
raise LoginError(ret.status_code, f'
|
|
270
|
+
raise LoginError(ret.status_code, f'获取跳转位置失败, {ret.text}')
|
|
272
271
|
cookies = self.session.cookies.get_dict()
|
|
273
272
|
|
|
274
273
|
self.auth_data = {
|
|
@@ -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 = "2.0.
|
|
3
|
+
version = "2.0.2"
|
|
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 = "2.0.
|
|
18
|
+
version = "2.0.2"
|
|
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
|
|
File without changes
|