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.
- {mijiaapi-1.3.3 → mijiaapi-1.3.5}/PKG-INFO +3 -3
- {mijiaapi-1.3.3 → mijiaapi-1.3.5}/mijiaAPI/apis.py +169 -169
- {mijiaapi-1.3.3 → mijiaapi-1.3.5}/mijiaAPI/devices.py +23 -15
- {mijiaapi-1.3.3 → mijiaapi-1.3.5}/mijiaAPI/login.py +163 -155
- {mijiaapi-1.3.3 → mijiaapi-1.3.5}/mijiaAPI/urls.py +6 -6
- {mijiaapi-1.3.3 → mijiaapi-1.3.5}/mijiaAPI/utils.py +43 -43
- {mijiaapi-1.3.3 → mijiaapi-1.3.5}/pyproject.toml +1 -1
- {mijiaapi-1.3.3 → mijiaapi-1.3.5}/LICENSE +0 -0
- {mijiaapi-1.3.3 → mijiaapi-1.3.5}/README.md +0 -0
- {mijiaapi-1.3.3 → mijiaapi-1.3.5}/mijiaAPI/__init__.py +0 -0
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: mijiaAPI
|
|
3
|
-
Version: 1.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
|
-
|
|
73
|
-
|
|
74
|
-
if prop.range:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
if prop.range:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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']
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
from .
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
self.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
self.
|
|
26
|
-
self.session.
|
|
27
|
-
|
|
28
|
-
'
|
|
29
|
-
'Accept
|
|
30
|
-
'Accept-
|
|
31
|
-
'
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
data.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
'
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
'
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
'
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if
|
|
82
|
-
raise LoginError(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
'
|
|
119
|
-
'
|
|
120
|
-
'
|
|
121
|
-
'
|
|
122
|
-
'
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if
|
|
138
|
-
raise LoginError(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
'
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if
|
|
149
|
-
raise LoginError(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|