mijiaAPI 1.3.3__tar.gz → 1.3.5__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.
@@ -1,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: mijiaAPI
3
- Version: 1.3.3
3
+ Version: 1.3.5
4
4
  Summary: A Python API for Xiaomi Mijia
5
- Home-page: https://github.com/Do1e/mijia-api
6
5
  License: GPLv3
7
6
  Author: Do1e
8
7
  Author-email: dpj.email@qq.com
@@ -19,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.13
19
18
  Requires-Dist: pillow (>=11.0.0,<12.0.0)
20
19
  Requires-Dist: qrcode (>=8.0,<9.0)
21
20
  Requires-Dist: requests (>=2.32.3,<3.0.0)
21
+ Project-URL: Homepage, https://github.com/Do1e/mijia-api
22
22
  Project-URL: Repository, https://github.com/Do1e/mijia-api
23
23
  Description-Content-Type: text/markdown
24
24
 
@@ -1,169 +1,169 @@
1
- from typing import Union
2
- import requests
3
- import requests.cookies
4
-
5
- from .utils import defaultUA, post_data, PostDataError
6
-
7
- class mijiaAPI(object):
8
- def __init__(self, auth_data: dict):
9
- if any(k not in auth_data for k in ['userId', 'deviceId', 'ssecurity', 'serviceToken']):
10
- raise Exception('Invalid authorize data')
11
- self.userId = auth_data['userId']
12
- self.ssecurity = auth_data['ssecurity']
13
- self.session = requests.Session()
14
- self.session.headers.update({
15
- 'User-Agent': defaultUA,
16
- 'x-xiaomi-protocal-flag-cli': 'PROTOCAL-HTTP2',
17
- 'Cookie': f'PassportDeviceId={auth_data["deviceId"]};'
18
- f'userId={auth_data["userId"]};'
19
- f'serviceToken={auth_data["serviceToken"]};',
20
- })
21
-
22
- @staticmethod
23
- def _post_process(data: dict) -> Union[list, bool]:
24
- if data['code'] != 0:
25
- raise Exception(f'Failed to get data, {data["message"]}')
26
- return data['result']
27
-
28
- @property
29
- def available(self) -> bool:
30
- """check if the API is available"""
31
- uri = '/home/device_list'
32
- data = {"getVirtualModel": False, "getHuamiDevices": 0}
33
- try:
34
- post_data(self.session, self.ssecurity, uri, data)
35
- return True
36
- except PostDataError:
37
- return False
38
-
39
- def get_devices_list(self) -> list:
40
- """get devices list
41
- mijiaAPI.get_devices_list() -> list
42
- -------
43
- @return
44
- dict, devices list
45
- """
46
- uri = '/home/device_list'
47
- data = {"getVirtualModel": False, "getHuamiDevices": 0}
48
- return self._post_process(post_data(self.session, self.ssecurity, uri, data))
49
-
50
- def get_homes_list(self) -> list:
51
- """get homes list
52
- mijiaAPI.get_homes_list() -> list
53
- -------
54
- @return
55
- list, homes list, including rooms
56
- """
57
- uri = '/v2/homeroom/gethome'
58
- data = {"fg": False, "fetch_share": True, "fetch_share_dev": True, "limit": 300, "app_ver": 7}
59
- return self._post_process(post_data(self.session, self.ssecurity, uri, data))
60
-
61
- def get_scenes_list(self, home_id: str) -> list:
62
- """get scenes list
63
- set it in Mi Home APP -> Add -> Manual controls
64
- mijiaAPI.get_scenes_list(home_id: str) -> list
65
- -------
66
- @param
67
- home_id: str, room id, get from get_homes_list
68
- -------
69
- @return
70
- list, scenes list
71
- """
72
- uri = '/appgateway/miot/appsceneservice/AppSceneService/GetSceneList'
73
- data = {"home_id": home_id}
74
- return self._post_process(post_data(self.session, self.ssecurity, uri, data))
75
-
76
- def run_scene(self, scene_id: str) -> bool:
77
- """run scene
78
- mijiaAPI.run_scene(scene_id: str) -> bool
79
- -------
80
- @param
81
- scene_id: str, scene id, get from get_scenes_list
82
- -------
83
- @return
84
- dict, result
85
- """
86
- uri = '/appgateway/miot/appsceneservice/AppSceneService/RunScene'
87
- data = {"scene_id": scene_id, "trigger_key": "user.click"}
88
- return self._post_process(post_data(self.session, self.ssecurity, uri, data))
89
-
90
- def get_consumable_items(self, home_id: str) -> list:
91
- """get consumable items
92
- mijiaAPI.get_consumable_items(did: str) -> list
93
- -------
94
- @param
95
- home_id: str, room id, get from get_homes_list
96
- -------
97
- @return
98
- list, consumable items
99
- """
100
- uri = '/v2/home/standard_consumable_items'
101
- data = {"home_id": int(home_id), "owner_id": self.userId}
102
- return self._post_process(post_data(self.session, self.ssecurity, uri, data))
103
-
104
- def get_devices_prop(self, data: list) -> list:
105
- """get devices properties
106
- mijiaAPI.get_devices_prop(data: list) -> list
107
- -------
108
- @param
109
- data: list of dict
110
- dict keys:
111
- - did: str, device id, get from get_devices_list
112
- - siid: str, service id, get from https://home.miot-spec.com/spec/{model}, model from get_devices_list
113
- - piid: str, property id, get from https://home.miot-spec.com/spec/{model}, model from get_devices_list
114
- model yeelink.light.lamp4 as an example:
115
- [
116
- {"did": "1234567890", "siid": 2, "piid": 2}, # get the brightness
117
- {"did": "1234567890", "siid": 2, "piid": 3}, # get the color temperature
118
- ]
119
- -------
120
- @return
121
- list, device properties
122
- """
123
- uri = '/miotspec/prop/get'
124
- data = {"params": data}
125
- return self._post_process(post_data(self.session, self.ssecurity, uri, data))
126
-
127
- def set_devices_prop(self, data: list) -> list:
128
- """set devices properties
129
- mijiaAPI.set_devices_prop(data: list) -> list
130
- -------
131
- @param
132
- data: list of dict
133
- dict keys:
134
- - did: str, device id, get from get_devices_list
135
- - siid: str, service id, get from https://home.miot-spec.com/spec/{model}, model from get_devices_list
136
- - piid: str, property id, get from https://home.miot-spec.com/spec/{model}, model from get_devices_list
137
- - value: str, value to set
138
- model yeelink.light.lamp4 as an example:
139
- [
140
- {"did": "1234567890", "siid": 2, "piid": 2, "value": 50} # set the brightness to 50%
141
- {"did": "1234567890", "siid": 2, "piid": 3, "value": 2700} # set the color temperature to 2700K
142
- ]
143
- -------
144
- @return
145
- dict, result
146
- """
147
- uri = '/miotspec/prop/set'
148
- data = {"params": data}
149
- return self._post_process(post_data(self.session, self.ssecurity, uri, data))
150
-
151
- def run_action(self, data: dict) -> dict:
152
- """run action
153
- mijiaAPI.run_action(data: dict) -> dict
154
- @param
155
- data: dict
156
- dict keys:
157
- - did: str, device id, get from get_devices_list
158
- - siid: str, service id, get from https://home.miot-spec.com/spec/{model}, model from get_devices_list
159
- - aiid: str, action id, get from https://home.miot-spec.com/spec/{model}, model from get_devices_list
160
- - value: list, value to list
161
- model xiaomi.feeder.pi2001 as an example:
162
- {"did": "1234567890", "siid": 2, "aiid": 1, "value": [2]}, # Remote feeding (2 servings) of food
163
- -------
164
- @return
165
- dict, result
166
- """
167
- uri = '/miotspec/action'
168
- data = {"params": data}
169
- return self._post_process(post_data(self.session, self.ssecurity, uri, data))
1
+ from typing import Union
2
+ import requests
3
+ import requests.cookies
4
+
5
+ from .utils import defaultUA, post_data, PostDataError
6
+
7
+ class mijiaAPI(object):
8
+ def __init__(self, auth_data: dict):
9
+ if any(k not in auth_data for k in ['userId', 'deviceId', 'ssecurity', 'serviceToken']):
10
+ raise Exception('Invalid authorize data')
11
+ self.userId = auth_data['userId']
12
+ self.ssecurity = auth_data['ssecurity']
13
+ self.session = requests.Session()
14
+ self.session.headers.update({
15
+ 'User-Agent': defaultUA,
16
+ 'x-xiaomi-protocal-flag-cli': 'PROTOCAL-HTTP2',
17
+ 'Cookie': f'PassportDeviceId={auth_data["deviceId"]};'
18
+ f'userId={auth_data["userId"]};'
19
+ f'serviceToken={auth_data["serviceToken"]};',
20
+ })
21
+
22
+ @staticmethod
23
+ def _post_process(data: dict) -> Union[list, bool]:
24
+ if data['code'] != 0:
25
+ raise Exception(f'Failed to get data, {data["message"]}')
26
+ return data['result']
27
+
28
+ @property
29
+ def available(self) -> bool:
30
+ """check if the API is available"""
31
+ uri = '/home/device_list'
32
+ data = {"getVirtualModel": False, "getHuamiDevices": 0}
33
+ try:
34
+ post_data(self.session, self.ssecurity, uri, data)
35
+ return True
36
+ except PostDataError:
37
+ return False
38
+
39
+ def get_devices_list(self) -> list:
40
+ """get devices list
41
+ mijiaAPI.get_devices_list() -> list
42
+ -------
43
+ @return
44
+ dict, devices list
45
+ """
46
+ uri = '/home/device_list'
47
+ data = {"getVirtualModel": False, "getHuamiDevices": 0}
48
+ return self._post_process(post_data(self.session, self.ssecurity, uri, data))
49
+
50
+ def get_homes_list(self) -> list:
51
+ """get homes list
52
+ mijiaAPI.get_homes_list() -> list
53
+ -------
54
+ @return
55
+ list, homes list, including rooms
56
+ """
57
+ uri = '/v2/homeroom/gethome'
58
+ data = {"fg": False, "fetch_share": True, "fetch_share_dev": True, "limit": 300, "app_ver": 7}
59
+ return self._post_process(post_data(self.session, self.ssecurity, uri, data))
60
+
61
+ def get_scenes_list(self, home_id: str) -> list:
62
+ """get scenes list
63
+ set it in Mi Home APP -> Add -> Manual controls
64
+ mijiaAPI.get_scenes_list(home_id: str) -> list
65
+ -------
66
+ @param
67
+ home_id: str, room id, get from get_homes_list
68
+ -------
69
+ @return
70
+ list, scenes list
71
+ """
72
+ uri = '/appgateway/miot/appsceneservice/AppSceneService/GetSceneList'
73
+ data = {"home_id": home_id}
74
+ return self._post_process(post_data(self.session, self.ssecurity, uri, data))
75
+
76
+ def run_scene(self, scene_id: str) -> bool:
77
+ """run scene
78
+ mijiaAPI.run_scene(scene_id: str) -> bool
79
+ -------
80
+ @param
81
+ scene_id: str, scene id, get from get_scenes_list
82
+ -------
83
+ @return
84
+ dict, result
85
+ """
86
+ uri = '/appgateway/miot/appsceneservice/AppSceneService/RunScene'
87
+ data = {"scene_id": scene_id, "trigger_key": "user.click"}
88
+ return self._post_process(post_data(self.session, self.ssecurity, uri, data))
89
+
90
+ def get_consumable_items(self, home_id: str) -> list:
91
+ """get consumable items
92
+ mijiaAPI.get_consumable_items(did: str) -> list
93
+ -------
94
+ @param
95
+ home_id: str, room id, get from get_homes_list
96
+ -------
97
+ @return
98
+ list, consumable items
99
+ """
100
+ uri = '/v2/home/standard_consumable_items'
101
+ data = {"home_id": int(home_id), "owner_id": self.userId}
102
+ return self._post_process(post_data(self.session, self.ssecurity, uri, data))
103
+
104
+ def get_devices_prop(self, data: list) -> list:
105
+ """get devices properties
106
+ mijiaAPI.get_devices_prop(data: list) -> list
107
+ -------
108
+ @param
109
+ data: list of dict
110
+ dict keys:
111
+ - did: str, device id, get from get_devices_list
112
+ - siid: str, service id, get from https://home.miot-spec.com/spec/{model}, model from get_devices_list
113
+ - piid: str, property id, get from https://home.miot-spec.com/spec/{model}, model from get_devices_list
114
+ model yeelink.light.lamp4 as an example:
115
+ [
116
+ {"did": "1234567890", "siid": 2, "piid": 2}, # get the brightness
117
+ {"did": "1234567890", "siid": 2, "piid": 3}, # get the color temperature
118
+ ]
119
+ -------
120
+ @return
121
+ list, device properties
122
+ """
123
+ uri = '/miotspec/prop/get'
124
+ data = {"params": data}
125
+ return self._post_process(post_data(self.session, self.ssecurity, uri, data))
126
+
127
+ def set_devices_prop(self, data: list) -> list:
128
+ """set devices properties
129
+ mijiaAPI.set_devices_prop(data: list) -> list
130
+ -------
131
+ @param
132
+ data: list of dict
133
+ dict keys:
134
+ - did: str, device id, get from get_devices_list
135
+ - siid: str, service id, get from https://home.miot-spec.com/spec/{model}, model from get_devices_list
136
+ - piid: str, property id, get from https://home.miot-spec.com/spec/{model}, model from get_devices_list
137
+ - value: str, value to set
138
+ model yeelink.light.lamp4 as an example:
139
+ [
140
+ {"did": "1234567890", "siid": 2, "piid": 2, "value": 50} # set the brightness to 50%
141
+ {"did": "1234567890", "siid": 2, "piid": 3, "value": 2700} # set the color temperature to 2700K
142
+ ]
143
+ -------
144
+ @return
145
+ dict, result
146
+ """
147
+ uri = '/miotspec/prop/set'
148
+ data = {"params": data}
149
+ return self._post_process(post_data(self.session, self.ssecurity, uri, data))
150
+
151
+ def run_action(self, data: dict) -> dict:
152
+ """run action
153
+ mijiaAPI.run_action(data: dict) -> dict
154
+ @param
155
+ data: dict
156
+ dict keys:
157
+ - did: str, device id, get from get_devices_list
158
+ - siid: str, service id, get from https://home.miot-spec.com/spec/{model}, model from get_devices_list
159
+ - aiid: str, action id, get from https://home.miot-spec.com/spec/{model}, model from get_devices_list
160
+ - value: list, value to list
161
+ model xiaomi.feeder.pi2001 as an example:
162
+ {"did": "1234567890", "siid": 2, "aiid": 1, "value": [2]}, # Remote feeding (2 servings) of food
163
+ -------
164
+ @return
165
+ dict, result
166
+ """
167
+ uri = '/miotspec/action'
168
+ data = {"params": data}
169
+ return self._post_process(post_data(self.session, self.ssecurity, uri, data))
@@ -69,21 +69,21 @@ class mijiaDevices(object):
69
69
  if not isinstance(value, bool):
70
70
  raise ValueError(f'Invalid value for bool: {value}, should be True or False')
71
71
  elif prop.type in ['int', 'uint']:
72
- try:
73
- value = int(value)
74
- if prop.range:
75
- if value < prop.range[0] or value > prop.range[1]:
76
- raise ValueError(f'Value out of range: {value}, should be in range {prop.range}')
77
- except ValueError:
78
- raise ValueError(f'Invalid value for int: {value}, should be an integer')
72
+ value = int(value)
73
+ if prop.range:
74
+ if value < prop.range[0] or value > prop.range[1]:
75
+ raise ValueError(f'Value out of range: {value}, should be in range {prop.range[:2]}')
76
+ if len(prop.range) >= 3 and prop.range[2] != 1:
77
+ if (value - prop.range[0]) % prop.range[2] != 0:
78
+ raise ValueError(f'Invalid value: {value}, should be in range {prop.range[:2]} with step {prop.range[2]}')
79
79
  elif prop.type == 'float':
80
- try:
81
- value = float(value)
82
- if prop.range:
83
- if value < prop.range[0] or value > prop.range[1]:
84
- raise ValueError(f'Value out of range: {value}, should be in range {prop.range}')
85
- except ValueError:
86
- raise ValueError(f'Invalid value for float: {value}, should be a float')
80
+ value = float(value)
81
+ if prop.range:
82
+ if value < prop.range[0] or value > prop.range[1]:
83
+ raise ValueError(f'Value out of range: {value}, should be in range {prop.range[:2]}')
84
+ if len(prop.range) >= 3 and isinstance(prop.range[2], int):
85
+ if int(value - prop.range[0]) % prop.range[2] != 0:
86
+ raise ValueError(f'Invalid value: {value}, should be in range {prop.range[:2]} with step {prop.range[2]}')
87
87
  elif prop.type == 'string':
88
88
  if not isinstance(value, str):
89
89
  raise ValueError(f'Invalid value for string: {value}, should be a string')
@@ -166,6 +166,8 @@ def get_device_info(device_model: str) -> dict:
166
166
  result['actions'] = []
167
167
  services = content['props']['spec']['services']
168
168
 
169
+ properties_name = []
170
+ actions_name = []
169
171
  for siid in services:
170
172
  if 'properties' in services[siid]:
171
173
  for piid in services[siid]['properties']:
@@ -193,11 +195,17 @@ def get_device_info(device_model: str) -> dict:
193
195
  }
194
196
  }
195
197
  if item['range'] is not None:
196
- item['range'] = item['range'][:2]
198
+ item['range'] = item['range']
199
+ if item['name'] in properties_name:
200
+ item["name"] = f'{services[siid]['name']}-{item["name"]}'
201
+ properties_name.append(item['name'])
197
202
  result['properties'].append({k: None if v == 'none' else v for k, v in item.items()})
198
203
  if 'actions' in services[siid]:
199
204
  for aiid in services[siid]['actions']:
200
205
  act = services[siid]['actions'][aiid]
206
+ if act['name'] in actions_name:
207
+ act['name'] = f'{services[siid]["name"]}-{act["name"]}'
208
+ actions_name.append(act['name'])
201
209
  result['actions'].append({
202
210
  'name': act['name'],
203
211
  'description': act['description'],
@@ -1,155 +1,163 @@
1
- from typing import Tuple
2
- import hashlib
3
- import json
4
- import os
5
- import random
6
- import string
7
- import time
8
- from urllib import parse
9
-
10
- from qrcode import QRCode
11
- import requests
12
-
13
- from .urls import msgURL, loginURL, qrURL
14
- from .utils import defaultUA
15
-
16
- class LoginError(Exception):
17
- def __init__(self, code: int, message: str):
18
- self.code = code
19
- self.message = message
20
- super().__init__(f'Error code: {code}, message: {message}')
21
-
22
- class mijiaLogin(object):
23
- def __init__(self):
24
- self.deviceId = ''.join(random.sample(string.digits + string.ascii_letters, 16))
25
- self.session = requests.Session()
26
- self.session.headers.update({
27
- 'User-Agent': defaultUA,
28
- 'Accept': '*/*',
29
- 'Accept-Encoding': 'gzip, deflate, br, zstd',
30
- 'Accept-Language': 'zh-CN,zh;q=0.9',
31
- 'Cookie': f'deviceId={self.deviceId}; sdkVersion=3.4.1'
32
- })
33
-
34
- def _get_index(self) -> Tuple[requests.Session, dict]:
35
- ret = self.session.get(msgURL)
36
- if ret.status_code != 200:
37
- raise LoginError(ret.status_code, f'Failed to get index page, {ret.text}')
38
- ret_data = json.loads(ret.text[11:])
39
- data = {'deviceId': self.deviceId}
40
- data.update({
41
- k: v for k, v in ret_data.items() \
42
- if k in ['qs', '_sign', 'callback', 'location']
43
- })
44
- return data
45
-
46
- def login(self, username: str, password: str) -> dict:
47
- """login with username and password
48
- mijiaLogin.login(username: str, password: str) -> dict
49
- -------
50
- @param
51
- username: str, xiaomi account username(email/phone number/xiaomi id)
52
- password: str, xiaomi account password
53
- -------
54
- @return
55
- dict, data for authorization, including userId, ssecurity, deviceId, serviceToken
56
- """
57
- data = self._get_index()
58
- post_data = {
59
- 'qs': data['qs'],
60
- '_sign': data['_sign'],
61
- 'callback': data['callback'],
62
- 'sid': 'xiaomiio',
63
- '_json': 'true',
64
- 'user': username,
65
- 'hash': (hashlib.md5(password.encode()).hexdigest().upper() + '0' * 32)[:32],
66
- }
67
- ret = self.session.post(loginURL, data=post_data)
68
- if ret.status_code != 200:
69
- raise LoginError(ret.status_code, f'Failed to post login page, {ret.text}')
70
- ret_data = json.loads(ret.text[11:])
71
- if ret_data['code'] != 0:
72
- raise LoginError(ret_data['code'], ret_data['desc'])
73
- if 'location' not in ret_data:
74
- raise LoginError(-1, 'Failed to get location')
75
- auth_data = {
76
- 'userId': ret_data['userId'],
77
- 'ssecurity': ret_data['ssecurity'],
78
- 'deviceId': data['deviceId'],
79
- }
80
- ret = self.session.get(ret_data['location'])
81
- if ret.status_code != 200:
82
- raise LoginError(ret.status_code, f'Failed to get location, {ret.text}')
83
- cookies = self.session.cookies.get_dict()
84
- auth_data['serviceToken'] = cookies['serviceToken']
85
- self.auth_data = auth_data
86
- return auth_data
87
-
88
- @staticmethod
89
- def _print_qr(loginurl: str, box_size: int = 10) -> None:
90
- print('Scan the QR code below with Mi Home app')
91
- qr = QRCode(border=1, box_size=box_size)
92
- qr.add_data(loginurl)
93
- qr.make_image().save('qr.png')
94
- try:
95
- qr.print_ascii(invert=True, tty=True)
96
- except OSError:
97
- print('Failed to print QR code to terminal, please use the qr.png file in the current directory.')
98
-
99
- def QRlogin(self) -> dict:
100
- """login with QR code
101
- mijiaLogin.QRlogin() -> dict
102
- -------
103
- @return
104
- dict, data for authorization, including userId, ssecurity, deviceId, serviceToken
105
- """
106
- data = self._get_index()
107
- location = data['location']
108
- location_parsed = parse.parse_qs(parse.urlparse(location).query)
109
- params = {
110
- '_qrsize': 240,
111
- 'qs': data['qs'],
112
- 'bizDeviceType': '',
113
- 'callback': data['callback'],
114
- '_json': 'true',
115
- 'theme': '',
116
- 'sid': 'xiaomiio',
117
- 'needTheme': 'false',
118
- 'showActiveX': 'false',
119
- 'serviceParam': location_parsed['serviceParam'][0],
120
- '_local': 'zh_CN',
121
- '_sign': data['_sign'],
122
- '_dc': str(int(time.time() * 1000)),
123
- }
124
- url = qrURL + '?' + parse.urlencode(params)
125
- ret = self.session.get(url)
126
- if ret.status_code != 200:
127
- raise LoginError(ret.status_code, f'Failed to get QR code URL, {ret.text}')
128
- ret_data = json.loads(ret.text[11:])
129
- if ret_data['code'] != 0:
130
- raise LoginError(ret_data['code'], ret_data['desc'])
131
- loginurl = ret_data['loginUrl']
132
- self._print_qr(loginurl)
133
- try:
134
- ret = self.session.get(ret_data['lp'], timeout=60, headers={'Connection': 'keep-alive'})
135
- except requests.exceptions.Timeout:
136
- raise LoginError(-1, 'Timeout, please try again')
137
- if ret.status_code != 200:
138
- raise LoginError(ret.status_code, f'Failed to wait for login, {ret.text}')
139
- ret_data = json.loads(ret.text[11:])
140
- if ret_data['code'] != 0:
141
- raise LoginError(ret_data['code'], ret_data['desc'])
142
- auth_data = {
143
- 'userId': ret_data['userId'],
144
- 'ssecurity': ret_data['ssecurity'],
145
- 'deviceId': data['deviceId'],
146
- }
147
- ret = self.session.get(ret_data['location'])
148
- if ret.status_code != 200:
149
- raise LoginError(ret.status_code, f'Failed to get location, {ret.text}')
150
- cookies = self.session.cookies.get_dict()
151
- auth_data['serviceToken'] = cookies['serviceToken']
152
- self.auth_data = auth_data
153
- if os.path.exists('qr.png'):
154
- os.remove('qr.png')
155
- return auth_data
1
+ from typing import Tuple
2
+ import hashlib
3
+ import json
4
+ import os
5
+ import random
6
+ import string
7
+ import sys
8
+ import time
9
+ from urllib import parse
10
+
11
+ from qrcode import QRCode
12
+ import requests
13
+
14
+ from .urls import msgURL, loginURL, qrURL
15
+ from .utils import defaultUA
16
+
17
+ class LoginError(Exception):
18
+ def __init__(self, code: int, message: str):
19
+ self.code = code
20
+ self.message = message
21
+ super().__init__(f'Error code: {code}, message: {message}')
22
+
23
+ class mijiaLogin(object):
24
+ def __init__(self):
25
+ self.deviceId = ''.join(random.sample(string.digits + string.ascii_letters, 16))
26
+ self.session = requests.Session()
27
+ self.session.headers.update({
28
+ 'User-Agent': defaultUA,
29
+ 'Accept': '*/*',
30
+ 'Accept-Encoding': 'gzip, deflate, br, zstd',
31
+ 'Accept-Language': 'zh-CN,zh;q=0.9',
32
+ 'Cookie': f'deviceId={self.deviceId}; sdkVersion=3.4.1'
33
+ })
34
+
35
+ def _get_index(self) -> Tuple[requests.Session, dict]:
36
+ ret = self.session.get(msgURL)
37
+ if ret.status_code != 200:
38
+ raise LoginError(ret.status_code, f'Failed to get index page, {ret.text}')
39
+ ret_data = json.loads(ret.text[11:])
40
+ data = {'deviceId': self.deviceId}
41
+ data.update({
42
+ k: v for k, v in ret_data.items() \
43
+ if k in ['qs', '_sign', 'callback', 'location']
44
+ })
45
+ return data
46
+
47
+ def login(self, username: str, password: str) -> dict:
48
+ """login with username and password
49
+ mijiaLogin.login(username: str, password: str) -> dict
50
+ -------
51
+ @param
52
+ username: str, xiaomi account username(email/phone number/xiaomi id)
53
+ password: str, xiaomi account password
54
+ -------
55
+ @return
56
+ dict, data for authorization, including userId, ssecurity, deviceId, serviceToken
57
+ """
58
+ warning_msg = 'WARNING: there is a high probability of verification code with account and password. Please try other login methods'
59
+ if sys.stdout.isatty():
60
+ print(f'\033[33;1m{warning_msg}\033[0m')
61
+ else:
62
+ print(warning_msg)
63
+ data = self._get_index()
64
+ post_data = {
65
+ 'qs': data['qs'],
66
+ '_sign': data['_sign'],
67
+ 'callback': data['callback'],
68
+ 'sid': 'xiaomiio',
69
+ '_json': 'true',
70
+ 'user': username,
71
+ 'hash': (hashlib.md5(password.encode()).hexdigest().upper() + '0' * 32)[:32],
72
+ }
73
+ ret = self.session.post(loginURL, data=post_data)
74
+ if ret.status_code != 200:
75
+ raise LoginError(ret.status_code, f'Failed to post login page, {ret.text}')
76
+ ret_data = json.loads(ret.text[11:])
77
+ if ret_data['code'] != 0:
78
+ raise LoginError(ret_data['code'], ret_data['desc'])
79
+ if 'location' not in ret_data:
80
+ raise LoginError(-1, 'Failed to get location')
81
+ if 'notificationUrl' in ret_data:
82
+ raise LoginError(-1, 'Verification code required, please try other login methods')
83
+ auth_data = {
84
+ 'userId': ret_data['userId'],
85
+ 'ssecurity': ret_data['ssecurity'],
86
+ 'deviceId': data['deviceId'],
87
+ }
88
+ ret = self.session.get(ret_data['location'])
89
+ if ret.status_code != 200:
90
+ raise LoginError(ret.status_code, f'Failed to get location, {ret.text}')
91
+ cookies = self.session.cookies.get_dict()
92
+ auth_data['serviceToken'] = cookies['serviceToken']
93
+ self.auth_data = auth_data
94
+ return auth_data
95
+
96
+ @staticmethod
97
+ def _print_qr(loginurl: str, box_size: int = 10) -> None:
98
+ print('Scan the QR code below with Mi Home app')
99
+ qr = QRCode(border=1, box_size=box_size)
100
+ qr.add_data(loginurl)
101
+ qr.make_image().save('qr.png')
102
+ try:
103
+ qr.print_ascii(invert=True, tty=True)
104
+ except OSError:
105
+ print('Failed to print QR code to terminal, please use the qr.png file in the current directory.')
106
+
107
+ def QRlogin(self) -> dict:
108
+ """login with QR code
109
+ mijiaLogin.QRlogin() -> dict
110
+ -------
111
+ @return
112
+ dict, data for authorization, including userId, ssecurity, deviceId, serviceToken
113
+ """
114
+ data = self._get_index()
115
+ location = data['location']
116
+ location_parsed = parse.parse_qs(parse.urlparse(location).query)
117
+ params = {
118
+ '_qrsize': 240,
119
+ 'qs': data['qs'],
120
+ 'bizDeviceType': '',
121
+ 'callback': data['callback'],
122
+ '_json': 'true',
123
+ 'theme': '',
124
+ 'sid': 'xiaomiio',
125
+ 'needTheme': 'false',
126
+ 'showActiveX': 'false',
127
+ 'serviceParam': location_parsed['serviceParam'][0],
128
+ '_local': 'zh_CN',
129
+ '_sign': data['_sign'],
130
+ '_dc': str(int(time.time() * 1000)),
131
+ }
132
+ url = qrURL + '?' + parse.urlencode(params)
133
+ ret = self.session.get(url)
134
+ if ret.status_code != 200:
135
+ raise LoginError(ret.status_code, f'Failed to get QR code URL, {ret.text}')
136
+ ret_data = json.loads(ret.text[11:])
137
+ if ret_data['code'] != 0:
138
+ raise LoginError(ret_data['code'], ret_data['desc'])
139
+ loginurl = ret_data['loginUrl']
140
+ self._print_qr(loginurl)
141
+ try:
142
+ ret = self.session.get(ret_data['lp'], timeout=60, headers={'Connection': 'keep-alive'})
143
+ except requests.exceptions.Timeout:
144
+ raise LoginError(-1, 'Timeout, please try again')
145
+ if ret.status_code != 200:
146
+ raise LoginError(ret.status_code, f'Failed to wait for login, {ret.text}')
147
+ ret_data = json.loads(ret.text[11:])
148
+ if ret_data['code'] != 0:
149
+ raise LoginError(ret_data['code'], ret_data['desc'])
150
+ auth_data = {
151
+ 'userId': ret_data['userId'],
152
+ 'ssecurity': ret_data['ssecurity'],
153
+ 'deviceId': data['deviceId'],
154
+ }
155
+ ret = self.session.get(ret_data['location'])
156
+ if ret.status_code != 200:
157
+ raise LoginError(ret.status_code, f'Failed to get location, {ret.text}')
158
+ cookies = self.session.cookies.get_dict()
159
+ auth_data['serviceToken'] = cookies['serviceToken']
160
+ self.auth_data = auth_data
161
+ if os.path.exists('qr.png'):
162
+ os.remove('qr.png')
163
+ return auth_data
@@ -1,6 +1,6 @@
1
- sid = 'xiaomiio'
2
- msgURL = 'https://account.xiaomi.com/pass/serviceLogin?sid=%s&_json=true' % sid
3
- loginURL = 'https://account.xiaomi.com/pass/serviceLoginAuth2'
4
- qrURL = 'https://account.xiaomi.com/longPolling/loginUrl'
5
- apiURL = 'https://api.io.mi.com/app'
6
- deviceURL = 'https://home.miot-spec.com/spec/'
1
+ sid = 'xiaomiio'
2
+ msgURL = 'https://account.xiaomi.com/pass/serviceLogin?sid=%s&_json=true' % sid
3
+ loginURL = 'https://account.xiaomi.com/pass/serviceLoginAuth2'
4
+ qrURL = 'https://account.xiaomi.com/longPolling/loginUrl'
5
+ apiURL = 'https://api.io.mi.com/app'
6
+ deviceURL = 'https://home.miot-spec.com/spec/'
@@ -1,43 +1,43 @@
1
- import base64
2
- from hashlib import sha256
3
- import hmac
4
- import random
5
- import string
6
-
7
- import requests
8
-
9
- from .urls import apiURL
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
-
13
- class PostDataError(Exception):
14
- def __init__(self, code: int, message: str):
15
- self.code = code
16
- self.message = message
17
- super().__init__(f'Error code: {code}, message: {message}')
18
-
19
- def _generate_signed_nonce(secret: str, nonce: str) -> str:
20
- sha = sha256()
21
- sha.update(base64.b64decode(secret))
22
- sha.update(base64.b64decode(nonce))
23
- return base64.b64encode(sha.digest()).decode()
24
-
25
-
26
- def _generate_signature(uri: str, signedNonce: str, nonce: str, data: str) -> str:
27
- sign = '&'.join([uri, signedNonce, nonce, f'data={data}'])
28
- mac = hmac.new(base64.b64decode(signedNonce), digestmod='sha256')
29
- mac.update(sign.encode())
30
- return base64.b64encode(mac.digest()).decode()
31
-
32
-
33
- def post_data(session: requests.Session, ssecurity: str, uri: str, data: dict) -> dict:
34
- data = str(data).replace("'", '"').replace('True', 'true').replace('False', 'false')
35
- nonce = ''.join(random.sample(string.digits + string.ascii_letters, 16))
36
- signed_nonce = _generate_signed_nonce(ssecurity, nonce)
37
- signature = _generate_signature(uri, signed_nonce, nonce, data)
38
- post_data = {'_nonce': nonce, 'data': data, 'signature': signature}
39
- ret = session.post(apiURL + uri, data=post_data)
40
- if ret.status_code != 200:
41
- raise PostDataError(ret.status_code, f'Failed to post data, {ret.text}')
42
- ret_data = ret.json()
43
- return ret_data
1
+ import base64
2
+ from hashlib import sha256
3
+ import hmac
4
+ import random
5
+ import string
6
+
7
+ import requests
8
+
9
+ from .urls import apiURL
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
+
13
+ class PostDataError(Exception):
14
+ def __init__(self, code: int, message: str):
15
+ self.code = code
16
+ self.message = message
17
+ super().__init__(f'Error code: {code}, message: {message}')
18
+
19
+ def _generate_signed_nonce(secret: str, nonce: str) -> str:
20
+ sha = sha256()
21
+ sha.update(base64.b64decode(secret))
22
+ sha.update(base64.b64decode(nonce))
23
+ return base64.b64encode(sha.digest()).decode()
24
+
25
+
26
+ def _generate_signature(uri: str, signedNonce: str, nonce: str, data: str) -> str:
27
+ sign = '&'.join([uri, signedNonce, nonce, f'data={data}'])
28
+ mac = hmac.new(base64.b64decode(signedNonce), digestmod='sha256')
29
+ mac.update(sign.encode())
30
+ return base64.b64encode(mac.digest()).decode()
31
+
32
+
33
+ def post_data(session: requests.Session, ssecurity: str, uri: str, data: dict) -> dict:
34
+ data = str(data).replace("'", '"').replace('True', 'true').replace('False', 'false')
35
+ nonce = ''.join(random.sample(string.digits + string.ascii_letters, 16))
36
+ signed_nonce = _generate_signed_nonce(ssecurity, nonce)
37
+ signature = _generate_signature(uri, signed_nonce, nonce, data)
38
+ post_data = {'_nonce': nonce, 'data': data, 'signature': signature}
39
+ ret = session.post(apiURL + uri, data=post_data)
40
+ if ret.status_code != 200:
41
+ raise PostDataError(ret.status_code, f'Failed to post data, {ret.text}')
42
+ ret_data = ret.json()
43
+ return ret_data
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "mijiaAPI"
3
- version = "1.3.3"
3
+ version = "1.3.5"
4
4
  description = "A Python API for Xiaomi Mijia"
5
5
  authors = ["Do1e <dpj.email@qq.com>"]
6
6
  license = "GPLv3"
File without changes
File without changes
File without changes