python-aidot 0.3.2__tar.gz → 0.3.4__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.
- {python_aidot-0.3.2 → python_aidot-0.3.4}/PKG-INFO +1 -1
- {python_aidot-0.3.2 → python_aidot-0.3.4}/aidot/aes_utils.py +4 -2
- {python_aidot-0.3.2 → python_aidot-0.3.4}/aidot/client.py +67 -21
- {python_aidot-0.3.2 → python_aidot-0.3.4}/aidot/const.py +16 -2
- python_aidot-0.3.4/aidot/device_client.py +372 -0
- {python_aidot-0.3.2 → python_aidot-0.3.4}/aidot/discover.py +64 -36
- {python_aidot-0.3.2 → python_aidot-0.3.4}/aidot/exceptions.py +13 -0
- {python_aidot-0.3.2 → python_aidot-0.3.4}/aidot/login_const.py +1 -2
- {python_aidot-0.3.2 → python_aidot-0.3.4}/python_aidot.egg-info/PKG-INFO +1 -1
- {python_aidot-0.3.2 → python_aidot-0.3.4}/python_aidot.egg-info/SOURCES.txt +1 -1
- {python_aidot-0.3.2 → python_aidot-0.3.4}/setup.py +1 -1
- python_aidot-0.3.2/aidot/lan.py +0 -318
- {python_aidot-0.3.2 → python_aidot-0.3.4}/LICENSE +0 -0
- {python_aidot-0.3.2 → python_aidot-0.3.4}/README.md +0 -0
- {python_aidot-0.3.2 → python_aidot-0.3.4}/aidot/__init__.py +0 -0
- {python_aidot-0.3.2 → python_aidot-0.3.4}/python_aidot.egg-info/dependency_links.txt +0 -0
- {python_aidot-0.3.2 → python_aidot-0.3.4}/python_aidot.egg-info/requires.txt +0 -0
- {python_aidot-0.3.2 → python_aidot-0.3.4}/python_aidot.egg-info/top_level.txt +0 -0
- {python_aidot-0.3.2 → python_aidot-0.3.4}/setup.cfg +0 -0
|
@@ -2,7 +2,8 @@ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
|
2
2
|
from cryptography.hazmat.backends import default_backend
|
|
3
3
|
from cryptography.hazmat.primitives import padding
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
def aes_encrypt(plaintext, key):
|
|
6
7
|
padder = padding.PKCS7(algorithms.AES.block_size).padder()
|
|
7
8
|
padded_data = padder.update(plaintext) + padder.finalize()
|
|
8
9
|
|
|
@@ -13,6 +14,7 @@ def aes_encrypt(plaintext, key):
|
|
|
13
14
|
|
|
14
15
|
return ciphertext
|
|
15
16
|
|
|
17
|
+
|
|
16
18
|
def aes_decrypt(ciphertext, key):
|
|
17
19
|
cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend())
|
|
18
20
|
decryptor = cipher.decryptor()
|
|
@@ -22,4 +24,4 @@ def aes_decrypt(ciphertext, key):
|
|
|
22
24
|
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
|
|
23
25
|
plaintext = unpadder.update(decrypted_data) + unpadder.finalize()
|
|
24
26
|
|
|
25
|
-
return plaintext.decode()
|
|
27
|
+
return plaintext.decode()
|
|
@@ -1,34 +1,39 @@
|
|
|
1
1
|
"""The aidot integration."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import logging
|
|
4
|
-
from typing import Any, Optional
|
|
5
|
-
from aiohttp import ClientSession
|
|
6
5
|
import base64
|
|
7
6
|
import aiohttp
|
|
7
|
+
from aiohttp import ClientSession
|
|
8
|
+
from typing import Any, Optional
|
|
8
9
|
from cryptography.hazmat.backends import default_backend
|
|
9
10
|
from cryptography.hazmat.primitives import serialization
|
|
10
11
|
from cryptography.hazmat.primitives.asymmetric import padding
|
|
12
|
+
|
|
13
|
+
from .exceptions import AidotAuthFailed, AidotUserOrPassIncorrect
|
|
14
|
+
from .device_client import DeviceClient
|
|
15
|
+
from .discover import Discover
|
|
11
16
|
from .login_const import APP_ID, PUBLIC_KEY_PEM, BASE_URL
|
|
12
17
|
from .const import (
|
|
13
|
-
SUPPORTED_COUNTRYS,
|
|
14
|
-
DEFAULT_COUNTRY_NAME,
|
|
15
|
-
CONF_PRODUCT_ID,
|
|
16
|
-
CONF_ID,
|
|
17
|
-
CONF_PRODUCT,
|
|
18
18
|
CONF_ACCESS_TOKEN,
|
|
19
|
-
CONF_REFRESH_TOKEN,
|
|
20
|
-
CONF_TERMINAL,
|
|
21
19
|
CONF_APP_ID,
|
|
22
|
-
|
|
20
|
+
CONF_CODE,
|
|
23
21
|
CONF_COUNTRY,
|
|
24
|
-
|
|
22
|
+
CONF_DEVICE_LIST,
|
|
23
|
+
CONF_ID,
|
|
24
|
+
CONF_IPADDRESS,
|
|
25
25
|
CONF_PASSWORD,
|
|
26
|
-
|
|
26
|
+
CONF_PRODUCT,
|
|
27
|
+
CONF_PRODUCT_ID,
|
|
28
|
+
CONF_REFRESH_TOKEN,
|
|
29
|
+
CONF_REGION,
|
|
30
|
+
CONF_TERMINAL,
|
|
27
31
|
CONF_TOKEN,
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
CONF_USERNAME,
|
|
33
|
+
DEFAULT_COUNTRY_NAME,
|
|
34
|
+
SUPPORTED_COUNTRYS,
|
|
35
|
+
ServerErrorCode,
|
|
30
36
|
)
|
|
31
|
-
from .exceptions import AidotAuthFailed,AidotUserOrPassIncorrect
|
|
32
37
|
|
|
33
38
|
_LOGGER = logging.getLogger(__name__)
|
|
34
39
|
|
|
@@ -56,6 +61,8 @@ class AidotClient:
|
|
|
56
61
|
password: str = ""
|
|
57
62
|
country_name: str = DEFAULT_COUNTRY_NAME
|
|
58
63
|
login_info: dict[str, Any] = {}
|
|
64
|
+
_device_clients: dict[str:DeviceClient]
|
|
65
|
+
_discover: Discover = None
|
|
59
66
|
|
|
60
67
|
def __init__(
|
|
61
68
|
self,
|
|
@@ -69,13 +76,14 @@ class AidotClient:
|
|
|
69
76
|
self.country_name = country_name
|
|
70
77
|
self.username = username
|
|
71
78
|
self.password = password
|
|
72
|
-
self.
|
|
79
|
+
self._device_clients = {}
|
|
73
80
|
for item in SUPPORTED_COUNTRYS:
|
|
74
81
|
if item["name"] == self.country_name:
|
|
75
82
|
self._region = item["region"].lower()
|
|
76
83
|
self._base_url = f"https://prod-{self._region}-api.arnoo.com/v17"
|
|
77
84
|
break
|
|
78
85
|
if token is not None:
|
|
86
|
+
self.login_info = token.copy()
|
|
79
87
|
self.username = token[CONF_USERNAME]
|
|
80
88
|
self.password = token[CONF_PASSWORD]
|
|
81
89
|
self._region = token[CONF_REGION]
|
|
@@ -86,7 +94,7 @@ class AidotClient:
|
|
|
86
94
|
|
|
87
95
|
def get_identifier(self) -> str:
|
|
88
96
|
return f"{self._region}-{self.username}"
|
|
89
|
-
|
|
97
|
+
|
|
90
98
|
def update_password(self, password: str):
|
|
91
99
|
self.password = password
|
|
92
100
|
|
|
@@ -154,6 +162,7 @@ class AidotClient:
|
|
|
154
162
|
CONF_TOKEN: token,
|
|
155
163
|
CONF_APP_ID: APP_ID,
|
|
156
164
|
}
|
|
165
|
+
response_data = {}
|
|
157
166
|
try:
|
|
158
167
|
response = await self.session.get(url, headers=headers)
|
|
159
168
|
response_data = await response.json()
|
|
@@ -161,17 +170,19 @@ class AidotClient:
|
|
|
161
170
|
return response_data
|
|
162
171
|
except aiohttp.ClientError as e:
|
|
163
172
|
_LOGGER.info(f"async_get ClientError {e}")
|
|
164
|
-
code = response_data
|
|
173
|
+
code = response_data.get(CONF_CODE)
|
|
165
174
|
if code == ServerErrorCode.TOKEN_EXPIRED:
|
|
166
175
|
try:
|
|
167
176
|
await self.async_refresh_token()
|
|
168
177
|
return await self.async_session_get(params)
|
|
169
178
|
except AidotAuthFailed:
|
|
170
179
|
raise AidotAuthFailed
|
|
171
|
-
elif
|
|
180
|
+
elif (
|
|
181
|
+
code == ServerErrorCode.LOGIN_INVALID or code == 21027 or code == 21041
|
|
182
|
+
):
|
|
172
183
|
self.login_info[CONF_ACCESS_TOKEN] = None
|
|
173
184
|
raise AidotAuthFailed
|
|
174
|
-
return
|
|
185
|
+
return aiohttp.ClientError
|
|
175
186
|
|
|
176
187
|
async def async_get_products(self, product_ids: str):
|
|
177
188
|
"""Get device list."""
|
|
@@ -208,4 +219,39 @@ class AidotClient:
|
|
|
208
219
|
device[CONF_PRODUCT] = product
|
|
209
220
|
except Exception as e:
|
|
210
221
|
raise e
|
|
211
|
-
return {CONF_DEVICE_LIST:final_device_list}
|
|
222
|
+
return {CONF_DEVICE_LIST: final_device_list}
|
|
223
|
+
|
|
224
|
+
def get_device_client(self, device: dict[str:Any]) -> DeviceClient:
|
|
225
|
+
device_id = device.get(CONF_ID)
|
|
226
|
+
device_client: DeviceClient = self._device_clients.get(device_id)
|
|
227
|
+
if device_client is None:
|
|
228
|
+
device_client = DeviceClient(device, self.login_info)
|
|
229
|
+
self._device_clients[device_id] = device_client
|
|
230
|
+
asyncio.get_running_loop().create_task(device_client.ping_task())
|
|
231
|
+
if self._discover is not None:
|
|
232
|
+
ip = self._discover.discovered_device.get(device_id)
|
|
233
|
+
device_client.update_ip_address(ip)
|
|
234
|
+
return device_client
|
|
235
|
+
|
|
236
|
+
def start_discover(self) -> None:
|
|
237
|
+
if self._discover is not None:
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
def _discover_callback(dev_id, event: dict[str, str]) -> None:
|
|
241
|
+
device_ip = event[CONF_IPADDRESS]
|
|
242
|
+
device_client: DeviceClient = self._device_clients.get(dev_id)
|
|
243
|
+
if device_client is not None:
|
|
244
|
+
device_client.update_ip_address(device_ip)
|
|
245
|
+
|
|
246
|
+
self._discover = Discover(self.login_info, _discover_callback)
|
|
247
|
+
asyncio.get_running_loop().create_task(self._discover.repeat_broadcast())
|
|
248
|
+
|
|
249
|
+
def stop_discover(self) -> None:
|
|
250
|
+
self._discover.close()
|
|
251
|
+
self._discover = None
|
|
252
|
+
|
|
253
|
+
def cleanup(self) -> None:
|
|
254
|
+
self.stop_discover()
|
|
255
|
+
for client in self._device_clients.values():
|
|
256
|
+
asyncio.get_running_loop().create_task(client.close())
|
|
257
|
+
self._device_clients.clear()
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from enum import StrEnum,IntEnum
|
|
1
|
+
from enum import StrEnum, IntEnum
|
|
2
|
+
|
|
2
3
|
SUPPORTED_COUNTRYS = [
|
|
3
4
|
{"_id": "1-0", "id": "AL", "name": "Albania", "ext": "", "region": "EU"},
|
|
4
5
|
{
|
|
@@ -195,17 +196,30 @@ CONF_MINVALUE = "minValue"
|
|
|
195
196
|
CONF_MAXVALUE = "maxValue"
|
|
196
197
|
CONF_IPADDRESS = "ipAddress"
|
|
197
198
|
CONF_CODE = "code"
|
|
199
|
+
CONF_PAYLOAD = "payload"
|
|
200
|
+
CONF_ASCNUMBER = "ascNumber"
|
|
201
|
+
CONF_ATTR = "attr"
|
|
202
|
+
CONF_ON_OFF = "OnOff"
|
|
203
|
+
CONF_DIMMING = "Dimming"
|
|
204
|
+
CONF_RGBW = "RGBW"
|
|
205
|
+
CONF_CCT = "CCT"
|
|
206
|
+
|
|
207
|
+
|
|
198
208
|
class Identity(StrEnum):
|
|
199
209
|
"""Available entity identity."""
|
|
210
|
+
|
|
200
211
|
RGBW = "control.light.rgbw"
|
|
201
212
|
CCT = "control.light.cct"
|
|
202
213
|
|
|
214
|
+
|
|
203
215
|
class Attribute(StrEnum):
|
|
204
216
|
"""Available entity attributes."""
|
|
217
|
+
|
|
205
218
|
RGBW = "rgbw"
|
|
206
219
|
CCT = "cct"
|
|
207
220
|
|
|
221
|
+
|
|
208
222
|
class ServerErrorCode(IntEnum):
|
|
209
223
|
TOKEN_EXPIRED = 21026
|
|
210
224
|
LOGIN_INVALID = 21025
|
|
211
|
-
USER_PWD_INCORRECT = 560080
|
|
225
|
+
USER_PWD_INCORRECT = 560080
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""The aidot integration."""
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
import socket
|
|
5
|
+
import struct
|
|
6
|
+
import time
|
|
7
|
+
import json
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .exceptions import AidotNotLogin
|
|
14
|
+
from .aes_utils import aes_encrypt, aes_decrypt
|
|
15
|
+
from .const import (
|
|
16
|
+
CONF_AES_KEY,
|
|
17
|
+
CONF_ASCNUMBER,
|
|
18
|
+
CONF_ATTR,
|
|
19
|
+
CONF_CCT,
|
|
20
|
+
CONF_HARDWARE_VERSION,
|
|
21
|
+
CONF_ID,
|
|
22
|
+
CONF_IDENTITY,
|
|
23
|
+
CONF_MAC,
|
|
24
|
+
CONF_MAXVALUE,
|
|
25
|
+
CONF_MINVALUE,
|
|
26
|
+
CONF_MODEL_ID,
|
|
27
|
+
CONF_NAME,
|
|
28
|
+
CONF_ON_OFF,
|
|
29
|
+
CONF_DIMMING,
|
|
30
|
+
CONF_PASSWORD,
|
|
31
|
+
CONF_PAYLOAD,
|
|
32
|
+
CONF_PRODUCT,
|
|
33
|
+
CONF_PROPERTIES,
|
|
34
|
+
CONF_RGBW,
|
|
35
|
+
CONF_SERVICE_MODULES,
|
|
36
|
+
Identity,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
_LOGGER = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DeviceStatusData:
|
|
43
|
+
online: bool = False
|
|
44
|
+
on: bool = False
|
|
45
|
+
rgdb: int = None
|
|
46
|
+
rgbw: tuple[int, int, int, int] = None
|
|
47
|
+
cct: int = None
|
|
48
|
+
dimming: int = None
|
|
49
|
+
|
|
50
|
+
def update(self, attr: dict[str, Any]) -> None:
|
|
51
|
+
if attr is None:
|
|
52
|
+
return
|
|
53
|
+
if attr.get(CONF_ON_OFF) is not None:
|
|
54
|
+
self.on = attr.get(CONF_ON_OFF)
|
|
55
|
+
if attr.get(CONF_DIMMING) is not None:
|
|
56
|
+
self.dimming = int(attr.get(CONF_DIMMING) * 255 / 100)
|
|
57
|
+
if attr.get(CONF_RGBW) is not None:
|
|
58
|
+
self.rgdb = attr.get(CONF_RGBW)
|
|
59
|
+
rgbw = ctypes.c_uint32(self.rgdb).value
|
|
60
|
+
r = (rgbw >> 24) & 0xFF
|
|
61
|
+
g = (rgbw >> 16) & 0xFF
|
|
62
|
+
b = (rgbw >> 8) & 0xFF
|
|
63
|
+
w = rgbw & 0xFF
|
|
64
|
+
self.rgbw = (r, g, b, w)
|
|
65
|
+
if attr.get(CONF_CCT) is not None:
|
|
66
|
+
self.cct = attr.get(CONF_CCT)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class DeviceInformation:
|
|
70
|
+
enable_rgbw: bool = False
|
|
71
|
+
enable_dimming: bool = True
|
|
72
|
+
enable_cct: bool = False
|
|
73
|
+
cct_min: int
|
|
74
|
+
cct_max: int
|
|
75
|
+
dev_id: str
|
|
76
|
+
mac: str
|
|
77
|
+
model_id: str
|
|
78
|
+
name: str
|
|
79
|
+
hw_version: str
|
|
80
|
+
|
|
81
|
+
def __init__(self, device: dict[str:Any]):
|
|
82
|
+
self.dev_id = device.get(CONF_ID)
|
|
83
|
+
self.mac = device.get(CONF_MAC) if device.get(CONF_MAC) is not None else ""
|
|
84
|
+
self.model_id = device.get(CONF_MODEL_ID)
|
|
85
|
+
self.name = device.get(CONF_NAME)
|
|
86
|
+
self.hw_version = device.get(CONF_HARDWARE_VERSION)
|
|
87
|
+
if CONF_PRODUCT in device and CONF_SERVICE_MODULES in device[CONF_PRODUCT]:
|
|
88
|
+
for service in device[CONF_PRODUCT][CONF_SERVICE_MODULES]:
|
|
89
|
+
if service[CONF_IDENTITY] == Identity.RGBW:
|
|
90
|
+
self.enable_rgbw = True
|
|
91
|
+
self.enable_cct = True
|
|
92
|
+
elif service[CONF_IDENTITY] == Identity.CCT:
|
|
93
|
+
self.cct_min = int(service[CONF_PROPERTIES][0][CONF_MINVALUE])
|
|
94
|
+
self.cct_max = int(service[CONF_PROPERTIES][0][CONF_MAXVALUE])
|
|
95
|
+
self.enable_cct = True
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class DeviceClient(object):
|
|
99
|
+
status: DeviceStatusData
|
|
100
|
+
info: DeviceInformation
|
|
101
|
+
_login_uuid = 0
|
|
102
|
+
_connect_and_login: bool = False
|
|
103
|
+
_connecting: bool = False
|
|
104
|
+
_simpleVersion: str = ""
|
|
105
|
+
_ip_address: str
|
|
106
|
+
device_id: str
|
|
107
|
+
_is_close: bool = False
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def connect_and_login(self) -> bool:
|
|
111
|
+
return self._connect_and_login
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def connecting(self) -> bool:
|
|
115
|
+
return self._connecting
|
|
116
|
+
|
|
117
|
+
def __init__(self, device: dict, user_info: dict) -> None:
|
|
118
|
+
self.ping_count = 0
|
|
119
|
+
self.status = DeviceStatusData()
|
|
120
|
+
self.info = DeviceInformation(device)
|
|
121
|
+
self.user_id = user_info.get(CONF_ID)
|
|
122
|
+
|
|
123
|
+
if CONF_AES_KEY in device:
|
|
124
|
+
key_string = device[CONF_AES_KEY][0]
|
|
125
|
+
if key_string is not None:
|
|
126
|
+
self.aes_key = bytearray(16)
|
|
127
|
+
key_bytes = key_string.encode()
|
|
128
|
+
self.aes_key[: len(key_bytes)] = key_bytes
|
|
129
|
+
|
|
130
|
+
self.password = device.get(CONF_PASSWORD)
|
|
131
|
+
self.device_id = device.get(CONF_ID)
|
|
132
|
+
self._simpleVersion = device.get("simpleVersion")
|
|
133
|
+
|
|
134
|
+
async def connect(self, ip_address):
|
|
135
|
+
self.reader = self.writer = None
|
|
136
|
+
self._connecting = True
|
|
137
|
+
try:
|
|
138
|
+
self.reader, self.writer = await asyncio.open_connection(ip_address, 10000)
|
|
139
|
+
sock: socket.socket = self.writer.get_extra_info("socket")
|
|
140
|
+
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|
141
|
+
self.seq_num = 1
|
|
142
|
+
await self.login()
|
|
143
|
+
self._connect_and_login = True
|
|
144
|
+
except Exception as e:
|
|
145
|
+
self._connect_and_login = False
|
|
146
|
+
finally:
|
|
147
|
+
self._connecting = False
|
|
148
|
+
|
|
149
|
+
def update_ip_address(self, ip: str) -> None:
|
|
150
|
+
self._ip_address = ip
|
|
151
|
+
|
|
152
|
+
async def async_login(self) -> None:
|
|
153
|
+
if self._ip_address is None:
|
|
154
|
+
return
|
|
155
|
+
if self._connecting is not True and self._connect_and_login is not True:
|
|
156
|
+
await self.connect(self._ip_address)
|
|
157
|
+
|
|
158
|
+
def getSendPacket(self, message, msgtype):
|
|
159
|
+
magic = struct.pack(">H", 0x1EED)
|
|
160
|
+
_msgtype = struct.pack(">h", msgtype)
|
|
161
|
+
|
|
162
|
+
if self.aes_key is not None:
|
|
163
|
+
send_data = aes_encrypt(message, self.aes_key)
|
|
164
|
+
else:
|
|
165
|
+
send_data = message
|
|
166
|
+
|
|
167
|
+
bodysize = struct.pack(">i", len(send_data))
|
|
168
|
+
packet = magic + _msgtype + bodysize + send_data
|
|
169
|
+
|
|
170
|
+
return packet
|
|
171
|
+
|
|
172
|
+
async def login(self):
|
|
173
|
+
login_seq = str(int(time.time() * 1000) + self._login_uuid)[-9:]
|
|
174
|
+
self._login_uuid += 1
|
|
175
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
|
|
176
|
+
message = {
|
|
177
|
+
"service": "device",
|
|
178
|
+
"method": "loginReq",
|
|
179
|
+
"seq": login_seq,
|
|
180
|
+
"srcAddr": self.user_id,
|
|
181
|
+
"deviceId": self.device_id,
|
|
182
|
+
"payload": {
|
|
183
|
+
"userId": self.user_id,
|
|
184
|
+
"password": self.password,
|
|
185
|
+
"timestamp": timestamp,
|
|
186
|
+
"ascNumber": 1,
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
try:
|
|
190
|
+
self.writer.write(self.getSendPacket(json.dumps(message).encode(), 1))
|
|
191
|
+
await self.writer.drain()
|
|
192
|
+
data = await self.reader.read(1024)
|
|
193
|
+
except (BrokenPipeError, ConnectionResetError) as e:
|
|
194
|
+
_LOGGER.error(f"{self.device_id} login read status error {e}")
|
|
195
|
+
except Exception as e:
|
|
196
|
+
_LOGGER.error(f"recv data error {e}")
|
|
197
|
+
|
|
198
|
+
data_len = len(data)
|
|
199
|
+
if data_len <= 0:
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
magic, msgtype, bodysize = struct.unpack(">HHI", data[:8])
|
|
203
|
+
encrypted_data = data[8:]
|
|
204
|
+
if self.aes_key is not None:
|
|
205
|
+
decrypted_data = aes_decrypt(encrypted_data, self.aes_key)
|
|
206
|
+
else:
|
|
207
|
+
decrypted_data = encrypted_data
|
|
208
|
+
|
|
209
|
+
json_data = json.loads(decrypted_data)
|
|
210
|
+
|
|
211
|
+
self.ascNumber = json_data[CONF_PAYLOAD][CONF_ASCNUMBER]
|
|
212
|
+
self.ascNumber += 1
|
|
213
|
+
self.status.online = True
|
|
214
|
+
await self.send_action({}, "getDevAttrReq")
|
|
215
|
+
|
|
216
|
+
async def read_status(self):
|
|
217
|
+
if self._connect_and_login is False:
|
|
218
|
+
await asyncio.sleep(2)
|
|
219
|
+
raise AidotNotLogin
|
|
220
|
+
try:
|
|
221
|
+
data = await self.reader.read(1024)
|
|
222
|
+
except (BrokenPipeError, ConnectionResetError) as e:
|
|
223
|
+
_LOGGER.error(f"{self.device_id} read status error {e}")
|
|
224
|
+
await self.reset()
|
|
225
|
+
self.status.online = False
|
|
226
|
+
return self.status
|
|
227
|
+
except Exception as e:
|
|
228
|
+
_LOGGER.error(f"recv data error {e}")
|
|
229
|
+
return self.status
|
|
230
|
+
data_len = len(data)
|
|
231
|
+
if data_len <= 0:
|
|
232
|
+
_LOGGER.error("recv data error len")
|
|
233
|
+
await self.reset()
|
|
234
|
+
self.status.online = False
|
|
235
|
+
return self.status
|
|
236
|
+
try:
|
|
237
|
+
magic, msgtype, bodysize = struct.unpack(">HHI", data[:8])
|
|
238
|
+
decrypted_data = aes_decrypt(data[8:], self.aes_key)
|
|
239
|
+
json_data = json.loads(decrypted_data)
|
|
240
|
+
except Exception as e:
|
|
241
|
+
_LOGGER.error(f"recv json error : {e}")
|
|
242
|
+
return await self.read_status()
|
|
243
|
+
|
|
244
|
+
if "service" in json_data:
|
|
245
|
+
if "test" == json_data["service"]:
|
|
246
|
+
self.ping_count = 0
|
|
247
|
+
return await self.read_status()
|
|
248
|
+
payload = json_data.get(CONF_PAYLOAD)
|
|
249
|
+
if payload is not None:
|
|
250
|
+
self.ascNumber = payload.get(CONF_ASCNUMBER)
|
|
251
|
+
self.status.update(payload.get(CONF_ATTR))
|
|
252
|
+
return self.status
|
|
253
|
+
|
|
254
|
+
async def ping_task(self):
|
|
255
|
+
while True:
|
|
256
|
+
if self._is_close:
|
|
257
|
+
return
|
|
258
|
+
await asyncio.sleep(5)
|
|
259
|
+
await self.send_ping_action()
|
|
260
|
+
await asyncio.sleep(5)
|
|
261
|
+
|
|
262
|
+
async def send_dev_attr(self, dev_attr):
|
|
263
|
+
await self.send_action(dev_attr, "setDevAttrReq")
|
|
264
|
+
|
|
265
|
+
async def async_turn_off(self) -> None:
|
|
266
|
+
await self.send_dev_attr({CONF_ON_OFF: 0})
|
|
267
|
+
|
|
268
|
+
async def async_turn_on(self) -> None:
|
|
269
|
+
await self.send_dev_attr({CONF_ON_OFF: 1})
|
|
270
|
+
|
|
271
|
+
async def async_set_brightness(self, brightness: int) -> None:
|
|
272
|
+
final_dimming = int(brightness * 100 / 255)
|
|
273
|
+
await self.send_dev_attr({CONF_DIMMING: final_dimming})
|
|
274
|
+
|
|
275
|
+
async def async_set_rgbw(self, rgbw: tuple[int, int, int, int]) -> None:
|
|
276
|
+
final_rgbw = (rgbw[0] << 24) | (rgbw[1] << 16) | (rgbw[2] << 8) | rgbw[3]
|
|
277
|
+
await self.send_dev_attr({CONF_RGBW: ctypes.c_int32(final_rgbw).value})
|
|
278
|
+
|
|
279
|
+
async def async_set_cct(self, cct: int) -> None:
|
|
280
|
+
await self.send_dev_attr({CONF_CCT: cct})
|
|
281
|
+
|
|
282
|
+
async def send_action(self, attr, method):
|
|
283
|
+
current_timestamp_milliseconds = int(time.time() * 1000)
|
|
284
|
+
self.seq_num += 1
|
|
285
|
+
seq = "ha93" + str(self.seq_num).zfill(5)
|
|
286
|
+
if not self.status.on and not CONF_ON_OFF in attr:
|
|
287
|
+
self.status.on = True
|
|
288
|
+
attr[CONF_ON_OFF] = 1
|
|
289
|
+
|
|
290
|
+
if self._simpleVersion is not None:
|
|
291
|
+
action = {
|
|
292
|
+
"method": method,
|
|
293
|
+
"service": "device",
|
|
294
|
+
"clientId": "ha-" + self.user_id,
|
|
295
|
+
"srcAddr": "0." + self.user_id,
|
|
296
|
+
"seq": "" + seq,
|
|
297
|
+
"payload": {
|
|
298
|
+
"devId": self.device_id,
|
|
299
|
+
"parentId": self.device_id,
|
|
300
|
+
"userId": self.user_id,
|
|
301
|
+
"password": self.password,
|
|
302
|
+
"attr": attr,
|
|
303
|
+
"channel": "tcp",
|
|
304
|
+
"ascNumber": self.ascNumber,
|
|
305
|
+
},
|
|
306
|
+
"tst": current_timestamp_milliseconds,
|
|
307
|
+
"deviceId": self.device_id,
|
|
308
|
+
}
|
|
309
|
+
else:
|
|
310
|
+
action = {
|
|
311
|
+
"method": method,
|
|
312
|
+
"service": "device",
|
|
313
|
+
"seq": "" + seq,
|
|
314
|
+
"srcAddr": "0." + self.user_id,
|
|
315
|
+
"payload": {
|
|
316
|
+
"attr": attr,
|
|
317
|
+
"ascNumber": self.ascNumber,
|
|
318
|
+
},
|
|
319
|
+
"tst": current_timestamp_milliseconds,
|
|
320
|
+
"deviceId": self.device_id,
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
self.writer.write(self.getSendPacket(json.dumps(action).encode(), 1))
|
|
325
|
+
await self.writer.drain()
|
|
326
|
+
except (BrokenPipeError, ConnectionResetError) as e:
|
|
327
|
+
_LOGGER.error(f"{self.device_id} send action error {e}")
|
|
328
|
+
await self.reset()
|
|
329
|
+
except Exception as e:
|
|
330
|
+
_LOGGER.error(f"{self.device_id} send action error {e}")
|
|
331
|
+
|
|
332
|
+
async def send_ping_action(self):
|
|
333
|
+
ping = {
|
|
334
|
+
"service": "test",
|
|
335
|
+
"method": "pingreq",
|
|
336
|
+
"seq": "123456",
|
|
337
|
+
"srcAddr": "x.xxxxxxx",
|
|
338
|
+
CONF_PAYLOAD: {},
|
|
339
|
+
}
|
|
340
|
+
try:
|
|
341
|
+
if self.ping_count >= 2:
|
|
342
|
+
_LOGGER.error(
|
|
343
|
+
f"Last ping did not return within 20 seconds. device id:{self.device_id}"
|
|
344
|
+
)
|
|
345
|
+
await self.reset()
|
|
346
|
+
return -1
|
|
347
|
+
if self._connect_and_login is False:
|
|
348
|
+
return -1
|
|
349
|
+
self.writer.write(self.getSendPacket(json.dumps(ping).encode(), 2))
|
|
350
|
+
await self.writer.drain()
|
|
351
|
+
self.ping_count += 1
|
|
352
|
+
return 1
|
|
353
|
+
except Exception as e:
|
|
354
|
+
_LOGGER.error(f"{self.device_id} ping error {e}")
|
|
355
|
+
await self.reset()
|
|
356
|
+
return -1
|
|
357
|
+
|
|
358
|
+
async def reset(self):
|
|
359
|
+
try:
|
|
360
|
+
if self.writer:
|
|
361
|
+
self.writer.close()
|
|
362
|
+
await self.writer.wait_closed()
|
|
363
|
+
except Exception as e:
|
|
364
|
+
_LOGGER.error(f"{self.device_id} writer close error {e}")
|
|
365
|
+
self._connect_and_login = False
|
|
366
|
+
self.status.online = False
|
|
367
|
+
self.ping_count = 0
|
|
368
|
+
|
|
369
|
+
async def close(self):
|
|
370
|
+
self._is_close = True
|
|
371
|
+
await self.reset()
|
|
372
|
+
_LOGGER.info(f"{self.device_id} connect close by user")
|
|
@@ -2,22 +2,24 @@ import socket
|
|
|
2
2
|
import json
|
|
3
3
|
import time
|
|
4
4
|
import logging
|
|
5
|
+
import asyncio
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
7
|
-
from .aes_utils import aes_encrypt,aes_decrypt
|
|
8
|
-
import
|
|
9
|
-
from .const import CONF_ID,CONF_IPADDRESS
|
|
8
|
+
from .aes_utils import aes_encrypt, aes_decrypt
|
|
9
|
+
from .const import CONF_ID, CONF_IPADDRESS
|
|
10
10
|
from .exceptions import AidotOSError
|
|
11
11
|
|
|
12
12
|
_LOGGER = logging.getLogger(__name__)
|
|
13
|
+
_DISCOVER_TIME = 5
|
|
13
14
|
|
|
14
15
|
class BroadcastProtocol:
|
|
15
16
|
_is_closed = False
|
|
16
|
-
|
|
17
|
+
|
|
18
|
+
def __init__(self, callback, user_id):
|
|
17
19
|
self.aes_key = bytearray(32)
|
|
18
20
|
key_string = "T54uednca587"
|
|
19
|
-
key_bytes = key_string.encode()
|
|
20
|
-
self.aes_key[:len(key_bytes)] = key_bytes
|
|
21
|
+
key_bytes = key_string.encode()
|
|
22
|
+
self.aes_key[: len(key_bytes)] = key_bytes
|
|
21
23
|
|
|
22
24
|
self._discover_cb = callback
|
|
23
25
|
self.user_id = user_id
|
|
@@ -34,43 +36,42 @@ class BroadcastProtocol:
|
|
|
34
36
|
current_timestamp_milliseconds = int(time.time() * 1000)
|
|
35
37
|
seq = str(current_timestamp_milliseconds + 1)[-9:]
|
|
36
38
|
message = {
|
|
37
|
-
"protocolVer":"2.0.0",
|
|
38
|
-
"service":"device",
|
|
39
|
-
"method":"devDiscoveryReq",
|
|
39
|
+
"protocolVer": "2.0.0",
|
|
40
|
+
"service": "device",
|
|
41
|
+
"method": "devDiscoveryReq",
|
|
40
42
|
"seq": seq,
|
|
41
|
-
"srcAddr":f"0.{self.user_id}]",
|
|
42
|
-
"tst":current_timestamp_milliseconds,
|
|
43
|
-
"payload":{
|
|
44
|
-
"extends":{
|
|
45
|
-
"localCtrFlag":1,
|
|
46
|
-
"timestamp":str(current_timestamp_milliseconds)
|
|
47
|
-
}
|
|
43
|
+
"srcAddr": f"0.{self.user_id}]",
|
|
44
|
+
"tst": current_timestamp_milliseconds,
|
|
45
|
+
"payload": {
|
|
46
|
+
"extends": {},
|
|
47
|
+
"localCtrFlag": 1,
|
|
48
|
+
"timestamp": str(current_timestamp_milliseconds),
|
|
49
|
+
},
|
|
48
50
|
}
|
|
49
|
-
send_data = aes_encrypt(json.dumps(message).encode(),self.aes_key)
|
|
51
|
+
send_data = aes_encrypt(json.dumps(message).encode(), self.aes_key)
|
|
50
52
|
try:
|
|
51
|
-
self.transport.sendto(send_data, (
|
|
53
|
+
self.transport.sendto(send_data, ("255.255.255.255", 6666))
|
|
52
54
|
except Exception as error:
|
|
53
55
|
_LOGGER.error(f"{self.user_id}:Connection lost due to error: {error}")
|
|
54
56
|
|
|
55
57
|
def datagram_received(self, data, addr):
|
|
56
|
-
data_str = aes_decrypt(data,self.aes_key)
|
|
58
|
+
data_str = aes_decrypt(data, self.aes_key)
|
|
57
59
|
data_json = json.loads(data_str)
|
|
58
|
-
if
|
|
59
|
-
if
|
|
60
|
+
if "payload" in data_json:
|
|
61
|
+
if "mac" in data_json["payload"]:
|
|
60
62
|
devId = data_json["payload"]["devId"]
|
|
61
63
|
if self._discover_cb:
|
|
62
|
-
|
|
64
|
+
self._discover_cb(devId, {CONF_IPADDRESS: addr[0]})
|
|
63
65
|
|
|
64
66
|
def error_received(self, exc):
|
|
65
67
|
_LOGGER.error(f"{self.user_id}:Error occurred: {exc}")
|
|
66
|
-
|
|
68
|
+
|
|
67
69
|
def close(self) -> None:
|
|
68
70
|
try:
|
|
69
71
|
self.transport.close()
|
|
70
72
|
except Exception as error:
|
|
71
73
|
_LOGGER.error(f"Connection lost due to error: {error}")
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
|
|
74
75
|
def connection_lost(self, exc):
|
|
75
76
|
self._is_closed = True
|
|
76
77
|
if exc:
|
|
@@ -78,33 +79,60 @@ class BroadcastProtocol:
|
|
|
78
79
|
else:
|
|
79
80
|
_LOGGER.info("{self.user_id}:Connection closed.")
|
|
80
81
|
|
|
82
|
+
|
|
81
83
|
class Discover:
|
|
82
|
-
_login_info: dict[str:
|
|
84
|
+
_login_info: dict[str:Any] = None
|
|
83
85
|
_broadcast_protocol: BroadcastProtocol = None
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
discovered_device: dict[str:str]
|
|
87
|
+
_is_close: bool = False
|
|
88
|
+
|
|
89
|
+
def __init__(self, login_info, callback):
|
|
90
|
+
self.discovered_device = {}
|
|
87
91
|
self._login_info = login_info
|
|
92
|
+
self._callback = callback
|
|
88
93
|
|
|
89
|
-
async def
|
|
94
|
+
async def try_create_broadcast(self) -> None:
|
|
90
95
|
if self._broadcast_protocol is None:
|
|
91
|
-
self._broadcast_protocol = BroadcastProtocol(
|
|
96
|
+
self._broadcast_protocol = BroadcastProtocol(
|
|
97
|
+
self._discover_callback, self._login_info[CONF_ID]
|
|
98
|
+
)
|
|
92
99
|
try:
|
|
93
|
-
|
|
100
|
+
(
|
|
101
|
+
transport,
|
|
102
|
+
protocol,
|
|
103
|
+
) = await asyncio.get_event_loop().create_datagram_endpoint(
|
|
94
104
|
lambda: self._broadcast_protocol,
|
|
95
105
|
local_addr=("0.0.0.0", 0),
|
|
96
106
|
)
|
|
97
107
|
except OSError:
|
|
98
108
|
raise AidotOSError
|
|
99
109
|
|
|
110
|
+
async def send_broadcast(self) -> None:
|
|
111
|
+
await self.try_create_broadcast()
|
|
112
|
+
self._broadcast_protocol.send_broadcast()
|
|
113
|
+
|
|
114
|
+
async def repeat_broadcast(self) -> None:
|
|
115
|
+
self._is_close = False
|
|
116
|
+
while True:
|
|
117
|
+
await self.send_broadcast()
|
|
118
|
+
for i in range(_DISCOVER_TIME):
|
|
119
|
+
await asyncio.sleep(1) # 每秒检查一次是否需要取消任务
|
|
120
|
+
if self._is_close is True:
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
async def fetch_devices_info(self) -> dict[str:str]:
|
|
124
|
+
self.try_create_broadcast()
|
|
100
125
|
self._broadcast_protocol.send_broadcast()
|
|
101
126
|
await asyncio.sleep(2)
|
|
102
|
-
return self.
|
|
127
|
+
return self.discovered_device
|
|
103
128
|
|
|
104
129
|
def _discover_callback(self, dev_id, event: dict[str, str]) -> None:
|
|
105
|
-
self.
|
|
106
|
-
|
|
130
|
+
self.discovered_device[dev_id] = event[CONF_IPADDRESS]
|
|
131
|
+
if self._callback:
|
|
132
|
+
self._callback(dev_id, event)
|
|
133
|
+
|
|
107
134
|
def close(self) -> None:
|
|
135
|
+
self._is_close = True
|
|
108
136
|
if self._broadcast_protocol is not None:
|
|
109
137
|
self._broadcast_protocol.close()
|
|
110
|
-
|
|
138
|
+
self._broadcast_protocol = None
|
|
@@ -1,24 +1,37 @@
|
|
|
1
1
|
"""aidot Exceptions."""
|
|
2
|
+
|
|
3
|
+
|
|
2
4
|
class AidotError(Exception):
|
|
3
5
|
"""Aidot api exception."""
|
|
4
6
|
|
|
7
|
+
|
|
5
8
|
class InvalidURL(AidotError):
|
|
6
9
|
"""Invalid url exception."""
|
|
7
10
|
|
|
11
|
+
|
|
8
12
|
class HTTPError(AidotError):
|
|
9
13
|
"""Invalid host exception."""
|
|
10
14
|
|
|
15
|
+
|
|
11
16
|
class InvalidHost(AidotError):
|
|
12
17
|
"""Invalid host exception."""
|
|
13
18
|
|
|
19
|
+
|
|
14
20
|
class AidotAuthTokenExpired(AidotError):
|
|
15
21
|
"""Authentication failed because token is invalid or expired."""
|
|
16
22
|
|
|
23
|
+
|
|
17
24
|
class AidotAuthFailed(AidotError):
|
|
18
25
|
"""Authentication failed"""
|
|
19
26
|
|
|
27
|
+
|
|
28
|
+
class AidotNotLogin(AidotError):
|
|
29
|
+
"""Aidot not login"""
|
|
30
|
+
|
|
31
|
+
|
|
20
32
|
class AidotUserOrPassIncorrect(AidotError):
|
|
21
33
|
"""Authentication failed"""
|
|
22
34
|
|
|
35
|
+
|
|
23
36
|
class AidotOSError(Exception):
|
|
24
37
|
"""Aidot exception."""
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Constants for the aidot integration."""
|
|
2
2
|
|
|
3
|
-
|
|
4
3
|
APP_ID = "1383974540041977857"
|
|
5
4
|
BASE_URL = "https://prod-us-api.arnoo.com/v17"
|
|
6
5
|
|
|
@@ -11,4 +10,4 @@ p2Gp/f/bHwlrAdplbX3p7/TnGpnbJGkLq8uRxf6cw+vOthTsZjkPCF7CatRvRnTj
|
|
|
11
10
|
c9fcy7yE0oXa5TloYyXD6GkxgftBbN/movkJJGQCc7gFavuYoAdTRBOyQoXBtm0m
|
|
12
11
|
kXMSjXOldI/290b9BQIDAQAB
|
|
13
12
|
-----END PUBLIC KEY-----
|
|
14
|
-
"""
|
|
13
|
+
"""
|
python_aidot-0.3.2/aidot/lan.py
DELETED
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
import socket
|
|
2
|
-
import struct
|
|
3
|
-
import binascii
|
|
4
|
-
import time
|
|
5
|
-
from datetime import datetime
|
|
6
|
-
import json
|
|
7
|
-
import asyncio
|
|
8
|
-
import logging
|
|
9
|
-
|
|
10
|
-
from .aes_utils import aes_encrypt,aes_decrypt
|
|
11
|
-
_LOGGER = logging.getLogger(__name__)
|
|
12
|
-
|
|
13
|
-
class Lan(object):
|
|
14
|
-
|
|
15
|
-
_is_on : bool = False
|
|
16
|
-
_dimming = 0
|
|
17
|
-
_rgdb : int
|
|
18
|
-
_cct : int
|
|
19
|
-
_login_uuid = 0
|
|
20
|
-
_available : bool = False
|
|
21
|
-
|
|
22
|
-
_connectAndLogin : bool = False
|
|
23
|
-
_connecting = False
|
|
24
|
-
_simpleVersion = ""
|
|
25
|
-
_colorMode = ""
|
|
26
|
-
|
|
27
|
-
@property
|
|
28
|
-
def is_on(self) -> bool:
|
|
29
|
-
return self._is_on
|
|
30
|
-
|
|
31
|
-
@property
|
|
32
|
-
def brightness(self) -> int:
|
|
33
|
-
return self._dimming * 255 / 100
|
|
34
|
-
|
|
35
|
-
@property
|
|
36
|
-
def rgdb(self) -> int:
|
|
37
|
-
return self._rgdb
|
|
38
|
-
|
|
39
|
-
@property
|
|
40
|
-
def cct(self) -> int:
|
|
41
|
-
return self._cct
|
|
42
|
-
|
|
43
|
-
@property
|
|
44
|
-
def available(self) -> bool:
|
|
45
|
-
return self._available
|
|
46
|
-
|
|
47
|
-
@property
|
|
48
|
-
def connectAndLogin(self) -> bool:
|
|
49
|
-
return self._connectAndLogin
|
|
50
|
-
|
|
51
|
-
@property
|
|
52
|
-
def connecting(self) -> bool:
|
|
53
|
-
return self._connecting
|
|
54
|
-
|
|
55
|
-
@property
|
|
56
|
-
def colorMode(self) -> str:
|
|
57
|
-
return self._colorMode
|
|
58
|
-
|
|
59
|
-
def __init__(self,device:dict,user_info:dict) -> None:
|
|
60
|
-
self.ping_count = 0
|
|
61
|
-
|
|
62
|
-
if "id" in user_info:
|
|
63
|
-
self.user_id = user_info["id"]
|
|
64
|
-
|
|
65
|
-
if "aesKey" in device :
|
|
66
|
-
key_string = device["aesKey"][0]
|
|
67
|
-
if key_string is not None:
|
|
68
|
-
self.aes_key = bytearray(16)
|
|
69
|
-
key_bytes = key_string.encode()
|
|
70
|
-
self.aes_key[:len(key_bytes)] = key_bytes
|
|
71
|
-
|
|
72
|
-
if "password" in device:
|
|
73
|
-
self.password = device["password"]
|
|
74
|
-
|
|
75
|
-
if "id" in device:
|
|
76
|
-
self.device_id = device["id"]
|
|
77
|
-
|
|
78
|
-
if "simpleVersion" in device:
|
|
79
|
-
self._simpleVersion = device["simpleVersion"]
|
|
80
|
-
|
|
81
|
-
async def connect(self,ipAddress):
|
|
82
|
-
self.reader = self.writer = None
|
|
83
|
-
self._connecting = True
|
|
84
|
-
try:
|
|
85
|
-
self.reader, self.writer = await asyncio.open_connection(ipAddress, 10000)
|
|
86
|
-
sock: socket.socket = self.writer.get_extra_info("socket")
|
|
87
|
-
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|
88
|
-
self.seq_num = 1
|
|
89
|
-
await self.login()
|
|
90
|
-
self._connectAndLogin = True
|
|
91
|
-
except Exception as e:
|
|
92
|
-
self._connectAndLogin = False
|
|
93
|
-
finally:
|
|
94
|
-
self._connecting = False
|
|
95
|
-
|
|
96
|
-
def setUpdateDeviceCb(self,callback):
|
|
97
|
-
self._updateDeviceCb = callback
|
|
98
|
-
|
|
99
|
-
def printfHex(self,packet):
|
|
100
|
-
hex_representation = binascii.hexlify(packet).decode()
|
|
101
|
-
|
|
102
|
-
def getSendPacket(self,message,msgtype):
|
|
103
|
-
magic = struct.pack('>H', 0x1eed)
|
|
104
|
-
_msgtype = struct.pack('>h', msgtype)
|
|
105
|
-
|
|
106
|
-
if self.aes_key is not None:
|
|
107
|
-
send_data = aes_encrypt(message,self.aes_key)
|
|
108
|
-
else :
|
|
109
|
-
send_data = message
|
|
110
|
-
|
|
111
|
-
bodysize = struct.pack('>i', len(send_data))
|
|
112
|
-
packet = magic + _msgtype + bodysize + send_data
|
|
113
|
-
|
|
114
|
-
return packet
|
|
115
|
-
|
|
116
|
-
async def login(self):
|
|
117
|
-
login_seq = str(int(time.time() * 1000) + self._login_uuid)[-9:]
|
|
118
|
-
self._login_uuid += 1
|
|
119
|
-
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
|
|
120
|
-
message = {
|
|
121
|
-
"service":"device",
|
|
122
|
-
"method":"loginReq",
|
|
123
|
-
"seq":login_seq,
|
|
124
|
-
"srcAddr":self.user_id,
|
|
125
|
-
"deviceId":self.device_id,
|
|
126
|
-
"payload":{
|
|
127
|
-
"userId":self.user_id,
|
|
128
|
-
"password":self.password,
|
|
129
|
-
"timestamp":timestamp,
|
|
130
|
-
"ascNumber":1
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
self.writer.write(self.getSendPacket(json.dumps(message).encode(),1))
|
|
134
|
-
await self.writer.drain()
|
|
135
|
-
|
|
136
|
-
data = await self.reader.read(1024)
|
|
137
|
-
data_len = len(data)
|
|
138
|
-
if(data_len <= 0):
|
|
139
|
-
return
|
|
140
|
-
|
|
141
|
-
magic, msgtype, bodysize = struct.unpack('>HHI', data[:8])
|
|
142
|
-
encrypted_data = data[8:]
|
|
143
|
-
if self.aes_key is not None:
|
|
144
|
-
decrypted_data = aes_decrypt(encrypted_data, self.aes_key)
|
|
145
|
-
else :
|
|
146
|
-
decrypted_data = encrypted_data
|
|
147
|
-
|
|
148
|
-
json_data = json.loads(decrypted_data)
|
|
149
|
-
|
|
150
|
-
self.ascNumber = json_data["payload"]["ascNumber"]
|
|
151
|
-
self.ascNumber += 1
|
|
152
|
-
|
|
153
|
-
self._available = True
|
|
154
|
-
|
|
155
|
-
await self.sendAction({},"getDevAttrReq")
|
|
156
|
-
|
|
157
|
-
async def recvData(self):
|
|
158
|
-
while True:
|
|
159
|
-
try :
|
|
160
|
-
data = await self.reader.read(1024)
|
|
161
|
-
except Exception as e:
|
|
162
|
-
_LOGGER.error(f"recv data error {e}")
|
|
163
|
-
await asyncio.sleep(3)
|
|
164
|
-
continue
|
|
165
|
-
data_len = len(data)
|
|
166
|
-
if(data_len <= 0):
|
|
167
|
-
break
|
|
168
|
-
|
|
169
|
-
try:
|
|
170
|
-
magic, msgtype, bodysize = struct.unpack('>HHI', data[:8])
|
|
171
|
-
encrypted_data = data[8:]
|
|
172
|
-
decrypted_data = aes_decrypt(encrypted_data, self.aes_key)
|
|
173
|
-
|
|
174
|
-
json_data = json.loads(decrypted_data)
|
|
175
|
-
except Exception as e:
|
|
176
|
-
_LOGGER.error(f"recv json error : {e}")
|
|
177
|
-
await asyncio.sleep(3)
|
|
178
|
-
continue
|
|
179
|
-
|
|
180
|
-
if "service" in json_data:
|
|
181
|
-
if "test" == json_data["service"]:
|
|
182
|
-
self.ping_count = 0
|
|
183
|
-
|
|
184
|
-
if "payload" in json_data:
|
|
185
|
-
if "ascNumber" in json_data["payload"]:
|
|
186
|
-
self.ascNumber = json_data["payload"]["ascNumber"]
|
|
187
|
-
if "attr" in json_data["payload"]:
|
|
188
|
-
if "OnOff" in json_data["payload"]["attr"]:
|
|
189
|
-
self._is_on = json_data["payload"]["attr"]["OnOff"]
|
|
190
|
-
if "Dimming" in json_data["payload"]["attr"]:
|
|
191
|
-
self._dimming = json_data["payload"]["attr"]["Dimming"]
|
|
192
|
-
if "RGBW" in json_data["payload"]["attr"]:
|
|
193
|
-
self._rgdb = json_data["payload"]["attr"]["RGBW"]
|
|
194
|
-
self._colorMode = "rgbw"
|
|
195
|
-
if "CCT" in json_data["payload"]["attr"]:
|
|
196
|
-
self._cct = json_data["payload"]["attr"]["CCT"]
|
|
197
|
-
self._colorMode = "cct"
|
|
198
|
-
if self._updateDeviceCb:
|
|
199
|
-
await self._updateDeviceCb()
|
|
200
|
-
|
|
201
|
-
async def ping_task(self):
|
|
202
|
-
while True:
|
|
203
|
-
await asyncio.sleep(5) #加个延迟,不然遇到查状态和发ping同时出现状态回不来,暂时不知道什么原因
|
|
204
|
-
if await self.sendPingAction() == -1 :
|
|
205
|
-
return
|
|
206
|
-
await asyncio.sleep(5)
|
|
207
|
-
|
|
208
|
-
def getOnOffAction(self,OnOff):
|
|
209
|
-
self._is_on = OnOff
|
|
210
|
-
return {"OnOff": self._is_on}
|
|
211
|
-
|
|
212
|
-
def getDimingAction(self,brightness):
|
|
213
|
-
self._dimming = int(brightness * 100 / 255)
|
|
214
|
-
return {"Dimming": self._dimming}
|
|
215
|
-
|
|
216
|
-
def getCCTAction(self,cct):
|
|
217
|
-
self._cct = cct
|
|
218
|
-
self._colorMode = "cct"
|
|
219
|
-
return {"CCT": self._cct}
|
|
220
|
-
|
|
221
|
-
def getRGBWAction(self,rgbw):
|
|
222
|
-
self._rgdb = rgbw
|
|
223
|
-
self._colorMode = "rgbw"
|
|
224
|
-
return {"RGBW": rgbw}
|
|
225
|
-
|
|
226
|
-
async def sendDevAttr(self,devAttr):
|
|
227
|
-
await self.sendAction(devAttr,"setDevAttrReq")
|
|
228
|
-
|
|
229
|
-
async def sendAction(self,attr,method):
|
|
230
|
-
|
|
231
|
-
current_timestamp_milliseconds = int(time.time() * 1000)
|
|
232
|
-
|
|
233
|
-
self.seq_num += 1
|
|
234
|
-
|
|
235
|
-
seq = "ha93" + str(self.seq_num).zfill(5)
|
|
236
|
-
|
|
237
|
-
if not self._is_on and not "OnOff" in attr:
|
|
238
|
-
attr["OnOff"] = 1
|
|
239
|
-
self._is_on = 1
|
|
240
|
-
|
|
241
|
-
if self._simpleVersion is not None:
|
|
242
|
-
action = {
|
|
243
|
-
"method": method,
|
|
244
|
-
"service": "device",
|
|
245
|
-
"clientId": "ha-" + self.user_id,
|
|
246
|
-
"srcAddr": "0." + self.user_id,
|
|
247
|
-
"seq": "" + seq,
|
|
248
|
-
"payload": {
|
|
249
|
-
"devId": self.device_id,
|
|
250
|
-
"parentId": self.device_id,
|
|
251
|
-
"userId": self.user_id,
|
|
252
|
-
"password": self.password,
|
|
253
|
-
"attr": attr,
|
|
254
|
-
"channel":"tcp",
|
|
255
|
-
"ascNumber":self.ascNumber,
|
|
256
|
-
},
|
|
257
|
-
"tst": current_timestamp_milliseconds,
|
|
258
|
-
# "tid": "homeassistant",
|
|
259
|
-
"deviceId": self.device_id,
|
|
260
|
-
}
|
|
261
|
-
else :
|
|
262
|
-
action = {
|
|
263
|
-
"method": method,
|
|
264
|
-
"service": "device",
|
|
265
|
-
"seq": "" + seq,
|
|
266
|
-
"srcAddr": "0." + self.user_id,
|
|
267
|
-
"payload": {
|
|
268
|
-
"attr": attr,
|
|
269
|
-
"ascNumber":self.ascNumber,
|
|
270
|
-
},
|
|
271
|
-
"tst": current_timestamp_milliseconds,
|
|
272
|
-
# "tid": "homeassistant",
|
|
273
|
-
"deviceId": self.device_id,
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
try:
|
|
277
|
-
self.writer.write(self.getSendPacket(json.dumps(action).encode(),1))
|
|
278
|
-
await self.writer.drain()
|
|
279
|
-
except BrokenPipeError as e :
|
|
280
|
-
_LOGGER.error(f"{self.device_id} send action error {e}")
|
|
281
|
-
except Exception as e:
|
|
282
|
-
_LOGGER.error(f"{self.device_id} send action error {e}")
|
|
283
|
-
|
|
284
|
-
async def sendPingAction(self):
|
|
285
|
-
ping = {
|
|
286
|
-
"service": "test",
|
|
287
|
-
"method": "pingreq",
|
|
288
|
-
"seq": "123456",
|
|
289
|
-
"srcAddr": "x.xxxxxxx",
|
|
290
|
-
"payload": {}
|
|
291
|
-
}
|
|
292
|
-
try:
|
|
293
|
-
if self.ping_count >= 2 :
|
|
294
|
-
_LOGGER.error(f"Last ping did not return within 20 seconds. device id:{self.device_id}")
|
|
295
|
-
await self.reset()
|
|
296
|
-
return -1
|
|
297
|
-
self.writer.write(self.getSendPacket(json.dumps(ping).encode(),2))
|
|
298
|
-
await self.writer.drain()
|
|
299
|
-
self.ping_count += 1
|
|
300
|
-
return 1
|
|
301
|
-
except Exception as e:
|
|
302
|
-
_LOGGER.error(f"{self.device_id} ping error {e}")
|
|
303
|
-
await self.reset()
|
|
304
|
-
return -1
|
|
305
|
-
|
|
306
|
-
async def reset(self):
|
|
307
|
-
try:
|
|
308
|
-
if self.writer:
|
|
309
|
-
self.writer.close()
|
|
310
|
-
await self.writer.wait_closed()
|
|
311
|
-
except Exception as e:
|
|
312
|
-
_LOGGER.error(f"{self.device_id} writer close error {e}")
|
|
313
|
-
self._connectAndLogin = False
|
|
314
|
-
self._available = False
|
|
315
|
-
self.ping_count = 0
|
|
316
|
-
if self._updateDeviceCb:
|
|
317
|
-
await self._updateDeviceCb()
|
|
318
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|