python-aidot 0.3.56__tar.gz → 0.3.58__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.56/python_aidot.egg-info → python_aidot-0.3.58}/PKG-INFO +1 -1
- {python_aidot-0.3.56 → python_aidot-0.3.58}/aidot/client.py +229 -2
- {python_aidot-0.3.56 → python_aidot-0.3.58}/aidot/const.py +8 -0
- {python_aidot-0.3.56 → python_aidot-0.3.58}/aidot/device_client.py +54 -4
- {python_aidot-0.3.56 → python_aidot-0.3.58}/aidot/login_const.py +1 -1
- {python_aidot-0.3.56 → python_aidot-0.3.58}/aidot/models/__init__.py +6 -0
- {python_aidot-0.3.56 → python_aidot-0.3.58}/aidot/models/device_client_model.py +2 -0
- {python_aidot-0.3.56 → python_aidot-0.3.58}/aidot/models/device_model.py +57 -0
- {python_aidot-0.3.56 → python_aidot-0.3.58/python_aidot.egg-info}/PKG-INFO +1 -1
- {python_aidot-0.3.56 → python_aidot-0.3.58}/setup.py +1 -1
- {python_aidot-0.3.56 → python_aidot-0.3.58}/LICENSE +0 -0
- {python_aidot-0.3.56 → python_aidot-0.3.58}/README.md +0 -0
- {python_aidot-0.3.56 → python_aidot-0.3.58}/aidot/__init__.py +0 -0
- {python_aidot-0.3.56 → python_aidot-0.3.58}/aidot/aes_utils.py +0 -0
- {python_aidot-0.3.56 → python_aidot-0.3.58}/aidot/discover.py +0 -0
- {python_aidot-0.3.56 → python_aidot-0.3.58}/aidot/exceptions.py +0 -0
- {python_aidot-0.3.56 → python_aidot-0.3.58}/aidot/models/discover_model.py +0 -0
- {python_aidot-0.3.56 → python_aidot-0.3.58}/python_aidot.egg-info/SOURCES.txt +0 -0
- {python_aidot-0.3.56 → python_aidot-0.3.58}/python_aidot.egg-info/dependency_links.txt +0 -0
- {python_aidot-0.3.56 → python_aidot-0.3.58}/python_aidot.egg-info/requires.txt +0 -0
- {python_aidot-0.3.56 → python_aidot-0.3.58}/python_aidot.egg-info/top_level.txt +0 -0
- {python_aidot-0.3.56 → python_aidot-0.3.58}/setup.cfg +0 -0
- {python_aidot-0.3.56 → python_aidot-0.3.58}/tests/test_client.py +0 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
5
|
import base64
|
|
6
|
+
import random
|
|
6
7
|
import aiohttp
|
|
7
8
|
from aiohttp import ClientSession
|
|
8
9
|
from typing import Any, Optional
|
|
@@ -14,6 +15,7 @@ from pathlib import Path
|
|
|
14
15
|
import hashlib
|
|
15
16
|
from .exceptions import AidotAuthFailed, AidotUserOrPassIncorrect
|
|
16
17
|
from .device_client import DeviceClient
|
|
18
|
+
from .models.device_model import EffectResp, FavoriteEffectPrimitive
|
|
17
19
|
from .discover import Discover
|
|
18
20
|
from .login_const import APP_ID, PUBLIC_KEY_PEM, API_URL_TEMPLATE, DEFAULT_REGION
|
|
19
21
|
from .const import (
|
|
@@ -38,6 +40,14 @@ from .const import (
|
|
|
38
40
|
CONF_IS_OWNER,
|
|
39
41
|
CONF_LOGIN_INFO,
|
|
40
42
|
ServerErrorCode,
|
|
43
|
+
CONF_MODEL_ID,
|
|
44
|
+
CONF_PROPERTIES,
|
|
45
|
+
CONF_LIGHT_SCRIPT_FLAGS,
|
|
46
|
+
CONF_PRESETS,
|
|
47
|
+
CONF_TYPE,
|
|
48
|
+
CONF_AES_KEY,
|
|
49
|
+
CONF_FIRMWARE_VERSION,
|
|
50
|
+
CONF_PRIMITIVE,
|
|
41
51
|
)
|
|
42
52
|
|
|
43
53
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -67,7 +77,7 @@ class AidotClient:
|
|
|
67
77
|
password: str | None = None,
|
|
68
78
|
token: dict | None = None,
|
|
69
79
|
) -> None:
|
|
70
|
-
_LOGGER.info("Client Version: v0.3.
|
|
80
|
+
_LOGGER.info("Client Version: v0.3.57")
|
|
71
81
|
self.session = session
|
|
72
82
|
self.username = username
|
|
73
83
|
self.password = password
|
|
@@ -77,6 +87,9 @@ class AidotClient:
|
|
|
77
87
|
self._base_url = API_URL_TEMPLATE.format(region=self._region)
|
|
78
88
|
self.login_info: dict[str, Any] = {}
|
|
79
89
|
self._device_clients = {}
|
|
90
|
+
self._effect_mode_params_cache: dict[
|
|
91
|
+
tuple[str, str, str, bool], dict[str, Any]
|
|
92
|
+
] = {}
|
|
80
93
|
self._discover: Discover | None = None
|
|
81
94
|
self._token_fresh_cb = None
|
|
82
95
|
for item in SUPPORTED_COUNTRYS:
|
|
@@ -222,6 +235,45 @@ class AidotClient:
|
|
|
222
235
|
raise AidotAuthFailed from err
|
|
223
236
|
raise
|
|
224
237
|
|
|
238
|
+
async def async_session_post(
|
|
239
|
+
self,
|
|
240
|
+
params: str,
|
|
241
|
+
data: Any,
|
|
242
|
+
headers: dict[str, str] | None = None,
|
|
243
|
+
) -> dict[str, Any]:
|
|
244
|
+
"""Post data to AiDot API."""
|
|
245
|
+
url = f"{self._base_url}{params}"
|
|
246
|
+
token = self.login_info[CONF_ACCESS_TOKEN]
|
|
247
|
+
if token is None:
|
|
248
|
+
raise AidotAuthFailed()
|
|
249
|
+
if headers is None:
|
|
250
|
+
headers = {
|
|
251
|
+
CONF_TERMINAL: "app",
|
|
252
|
+
CONF_TOKEN: token,
|
|
253
|
+
CONF_APP_ID: APP_ID,
|
|
254
|
+
}
|
|
255
|
+
response_data = {}
|
|
256
|
+
try:
|
|
257
|
+
response = await self.session.post(url, headers=headers, json=data)
|
|
258
|
+
response_data = await response.json()
|
|
259
|
+
response.raise_for_status()
|
|
260
|
+
return response_data
|
|
261
|
+
except aiohttp.ClientError as err:
|
|
262
|
+
_LOGGER.error("async_post ClientError: %s %s", err, response_data)
|
|
263
|
+
code = response_data.get(CONF_CODE)
|
|
264
|
+
if code == ServerErrorCode.TOKEN_EXPIRED:
|
|
265
|
+
try:
|
|
266
|
+
await self.async_refresh_token()
|
|
267
|
+
return await self.async_session_post(params, data)
|
|
268
|
+
except AidotAuthFailed as auth_err:
|
|
269
|
+
raise AidotAuthFailed from auth_err
|
|
270
|
+
elif (
|
|
271
|
+
code == ServerErrorCode.LOGIN_INVALID or code == 21027 or code == 21041
|
|
272
|
+
):
|
|
273
|
+
self.login_info[CONF_ACCESS_TOKEN] = None
|
|
274
|
+
raise AidotAuthFailed from err
|
|
275
|
+
raise
|
|
276
|
+
|
|
225
277
|
async def async_get_products(self, product_ids: str) -> list[dict[str, Any]]:
|
|
226
278
|
"""Get device list."""
|
|
227
279
|
params = f"/products/{product_ids}"
|
|
@@ -237,6 +289,173 @@ class AidotClient:
|
|
|
237
289
|
params = "/houses"
|
|
238
290
|
return await self.async_session_get(params)
|
|
239
291
|
|
|
292
|
+
async def async_get_diy_list(
|
|
293
|
+
self, device: dict[str, Any]
|
|
294
|
+
) -> list[FavoriteEffectPrimitive]:
|
|
295
|
+
"""Get favorite list."""
|
|
296
|
+
params = f"/devices/{device[CONF_ID]}/v4.0/favoriteEffectMode"
|
|
297
|
+
resp = await self.async_session_get(params)
|
|
298
|
+
return EffectResp.from_json(resp).primitive
|
|
299
|
+
|
|
300
|
+
async def async_get_fav_presets(
|
|
301
|
+
self, device: dict[str, Any]
|
|
302
|
+
) -> list[FavoriteEffectPrimitive]:
|
|
303
|
+
"""Get favorite list."""
|
|
304
|
+
# https://prod-us-api.arnoo.com/v35/devices/8a040e2233a243e7a07eb6a1b7a210a7/v4.0/favoriteEffectMode?libraryId=aidot.preset
|
|
305
|
+
|
|
306
|
+
params = (
|
|
307
|
+
f"/devices/{device[CONF_ID]}/v4.0/favoriteEffectMode?libraryId=aidot.preset"
|
|
308
|
+
)
|
|
309
|
+
resp = await self.async_session_get(params)
|
|
310
|
+
return EffectResp.from_json(resp).primitive
|
|
311
|
+
|
|
312
|
+
async def async_get_presets(
|
|
313
|
+
self, device: dict[str, Any]
|
|
314
|
+
) -> list[FavoriteEffectPrimitive]:
|
|
315
|
+
"""Get preset list."""
|
|
316
|
+
# https://prod-us-api.arnoo.com/v35/models/LK.light.A001855/v3.0/effectModes?libraryId=aidot.preset&version=3.30.09&lightScriptFlags=0002140503140500000062
|
|
317
|
+
|
|
318
|
+
params = (
|
|
319
|
+
f"/models/{device[CONF_MODEL_ID]}/v3.0/effectModes?libraryId=aidot.preset"
|
|
320
|
+
)
|
|
321
|
+
properties = device.get(CONF_PROPERTIES, {})
|
|
322
|
+
light_script_flags = properties.get(CONF_LIGHT_SCRIPT_FLAGS)
|
|
323
|
+
if light_script_flags:
|
|
324
|
+
params = (
|
|
325
|
+
f"{params}&version={device[CONF_FIRMWARE_VERSION]}"
|
|
326
|
+
f"&lightScriptFlags={light_script_flags}"
|
|
327
|
+
)
|
|
328
|
+
resp = await self.async_session_get(params)
|
|
329
|
+
return EffectResp.from_json(resp).primitive
|
|
330
|
+
|
|
331
|
+
async def async_get_effect_mode_params(
|
|
332
|
+
self,
|
|
333
|
+
device_id: str,
|
|
334
|
+
primitive: FavoriteEffectPrimitive,
|
|
335
|
+
) -> dict[str, Any]:
|
|
336
|
+
"""Get effect mode params."""
|
|
337
|
+
primitive_effect_id = primitive.primitiveEffectId
|
|
338
|
+
favorite_id = primitive.favoriteId
|
|
339
|
+
library_id = primitive.libraryId
|
|
340
|
+
if favorite_id is not None:
|
|
341
|
+
effect_query = f"favoriteIds={favorite_id}"
|
|
342
|
+
effect_cache_id = favorite_id
|
|
343
|
+
elif primitive_effect_id is not None:
|
|
344
|
+
effect_query = f"primitiveEffectIds={primitive_effect_id}"
|
|
345
|
+
effect_cache_id = primitive_effect_id
|
|
346
|
+
else:
|
|
347
|
+
raise ValueError(
|
|
348
|
+
"Effect primitive must have favoriteId or primitiveEffectId"
|
|
349
|
+
)
|
|
350
|
+
if library_id is None:
|
|
351
|
+
raise ValueError("Effect primitive must have libraryId")
|
|
352
|
+
|
|
353
|
+
cache_key = (device_id, effect_cache_id, library_id, primitive.isOldParams)
|
|
354
|
+
if cache_key in self._effect_mode_params_cache:
|
|
355
|
+
return self._effect_mode_params_cache[cache_key]
|
|
356
|
+
|
|
357
|
+
params = (
|
|
358
|
+
f"/devices/{device_id}/v3.0/effectModeParams"
|
|
359
|
+
f"?{effect_query}"
|
|
360
|
+
f"&isOldParams={primitive.isOldParams}"
|
|
361
|
+
f"&libraryId={library_id}"
|
|
362
|
+
)
|
|
363
|
+
resp = await self.async_session_get(params)
|
|
364
|
+
effect_primitives = resp.get(CONF_PRIMITIVE) or []
|
|
365
|
+
if not effect_primitives:
|
|
366
|
+
raise ValueError("Effect mode params response has no primitive")
|
|
367
|
+
|
|
368
|
+
effect_params = effect_primitives[0]
|
|
369
|
+
self._effect_mode_params_cache[cache_key] = effect_params
|
|
370
|
+
return effect_params
|
|
371
|
+
|
|
372
|
+
async def async_execute_diff_command(
|
|
373
|
+
self,
|
|
374
|
+
device_id: str,
|
|
375
|
+
primitive: FavoriteEffectPrimitive,
|
|
376
|
+
) -> dict[str, Any]:
|
|
377
|
+
"""Execute diff command."""
|
|
378
|
+
# effect_params = await self.async_get_effect_mode_params(device_id, primitive)
|
|
379
|
+
data = [
|
|
380
|
+
{
|
|
381
|
+
"devId": device_id,
|
|
382
|
+
"effectUniqueID": primitive.primitiveEffectId,
|
|
383
|
+
"action": "runLScript",
|
|
384
|
+
"in": [
|
|
385
|
+
{
|
|
386
|
+
"sessionId": random.randint(1, 2_147_483_647),
|
|
387
|
+
# "params": effect_params["params"],
|
|
388
|
+
}
|
|
389
|
+
],
|
|
390
|
+
}
|
|
391
|
+
]
|
|
392
|
+
return await self.async_session_post("/devices/execute/diffCommand", data)
|
|
393
|
+
|
|
394
|
+
async def async_get_all_effects(
|
|
395
|
+
self, device: dict[str, Any]
|
|
396
|
+
) -> dict[str, FavoriteEffectPrimitive]:
|
|
397
|
+
"""Get all effects list."""
|
|
398
|
+
diy_list: list[FavoriteEffectPrimitive] = []
|
|
399
|
+
fav_preset: list[FavoriteEffectPrimitive] = []
|
|
400
|
+
preset_list: list[FavoriteEffectPrimitive] = []
|
|
401
|
+
|
|
402
|
+
try:
|
|
403
|
+
diy_list = await self.async_get_diy_list(device)
|
|
404
|
+
except Exception as err:
|
|
405
|
+
_LOGGER.warning(
|
|
406
|
+
"Failed to get DIY effects for %s: %s", device[CONF_ID], err
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
try:
|
|
410
|
+
fav_preset = await self.async_get_fav_presets(device)
|
|
411
|
+
except Exception as err:
|
|
412
|
+
_LOGGER.warning(
|
|
413
|
+
"Failed to get favorite preset effects for %s: %s",
|
|
414
|
+
device[CONF_ID],
|
|
415
|
+
err,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
try:
|
|
419
|
+
preset_list = await self.async_get_presets(device)
|
|
420
|
+
except Exception as err:
|
|
421
|
+
_LOGGER.warning(
|
|
422
|
+
"Failed to get preset effects for %s: %s", device[CONF_ID], err
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
fav_preset_ids = {
|
|
426
|
+
item.primitiveEffectId
|
|
427
|
+
for item in fav_preset
|
|
428
|
+
if item.primitiveEffectId is not None
|
|
429
|
+
}
|
|
430
|
+
preset_list = [
|
|
431
|
+
item for item in preset_list if item.primitiveEffectId not in fav_preset_ids
|
|
432
|
+
]
|
|
433
|
+
return self._merge_effects_by_unique_name(diy_list, fav_preset, preset_list)
|
|
434
|
+
|
|
435
|
+
@staticmethod
|
|
436
|
+
def _merge_effects_by_unique_name(
|
|
437
|
+
*effect_lists: list[FavoriteEffectPrimitive],
|
|
438
|
+
) -> dict[str, FavoriteEffectPrimitive]:
|
|
439
|
+
"""Merge effect lists into a dict with unique display names."""
|
|
440
|
+
effects: dict[str, FavoriteEffectPrimitive] = {}
|
|
441
|
+
name_counts: dict[str, int] = {}
|
|
442
|
+
|
|
443
|
+
for effect_list in effect_lists:
|
|
444
|
+
for effect in effect_list:
|
|
445
|
+
name = (
|
|
446
|
+
effect.name
|
|
447
|
+
or effect.primitiveEffectId
|
|
448
|
+
or effect.favoriteId
|
|
449
|
+
or "Effect"
|
|
450
|
+
)
|
|
451
|
+
name_counts[name] = name_counts.get(name, 0) + 1
|
|
452
|
+
unique_name = (
|
|
453
|
+
name if name_counts[name] == 1 else f"{name} ({name_counts[name]})"
|
|
454
|
+
)
|
|
455
|
+
effects[unique_name] = effect
|
|
456
|
+
|
|
457
|
+
return effects
|
|
458
|
+
|
|
240
459
|
async def async_get_all_device(self) -> dict[str, Any]:
|
|
241
460
|
final_device_list: list[dict[str, Any]] = []
|
|
242
461
|
try:
|
|
@@ -260,6 +479,14 @@ class AidotClient:
|
|
|
260
479
|
if device[CONF_PRODUCT_ID] == product[CONF_ID]:
|
|
261
480
|
device[CONF_PRODUCT] = product
|
|
262
481
|
|
|
482
|
+
for device in final_device_list:
|
|
483
|
+
if (
|
|
484
|
+
device[CONF_TYPE] == "light"
|
|
485
|
+
and CONF_AES_KEY in device
|
|
486
|
+
and device[CONF_AES_KEY][0] is not None
|
|
487
|
+
):
|
|
488
|
+
device[CONF_PRESETS] = await self.async_get_all_effects(device)
|
|
489
|
+
|
|
263
490
|
except Exception as e:
|
|
264
491
|
raise e
|
|
265
492
|
return {CONF_DEVICE_LIST: final_device_list}
|
|
@@ -268,7 +495,7 @@ class AidotClient:
|
|
|
268
495
|
device_id = device.get(CONF_ID)
|
|
269
496
|
device_client: DeviceClient = self._device_clients.get(device_id)
|
|
270
497
|
if device_client is None:
|
|
271
|
-
device_client = DeviceClient(device, self.login_info)
|
|
498
|
+
device_client = DeviceClient(device, self.login_info, self)
|
|
272
499
|
self._device_clients[device_id] = device_client
|
|
273
500
|
if self._discover is not None:
|
|
274
501
|
ip = self._discover.discovered_device.get(device_id)
|
|
@@ -192,6 +192,7 @@ CONF_LOGIN_INFO = "loginInfo"
|
|
|
192
192
|
CONF_AES_KEY = "aesKey"
|
|
193
193
|
CONF_MODEL_ID = "modelId"
|
|
194
194
|
CONF_HARDWARE_VERSION = "hardwareVersion"
|
|
195
|
+
CONF_FIRMWARE_VERSION = "firmwareVersion"
|
|
195
196
|
CONF_SERVICE_MODULES = "serviceModules"
|
|
196
197
|
CONF_IDENTITY = "identity"
|
|
197
198
|
CONF_PROPERTIES = "properties"
|
|
@@ -206,10 +207,17 @@ CONF_ON_OFF = "OnOff"
|
|
|
206
207
|
CONF_DIMMING = "Dimming"
|
|
207
208
|
CONF_RGBW = "RGBW"
|
|
208
209
|
CONF_CCT = "CCT"
|
|
210
|
+
CONF_EFFECT_MODE = "EffectMode"
|
|
209
211
|
CONF_ACK = "ack"
|
|
210
212
|
CONF_IS_OWNER = "isOwner"
|
|
211
213
|
CONF_GET_DEV_ATTR_REQ = "getDevAttrReq"
|
|
212
214
|
CONF_SET_DEV_ATTR_REQ = "setDevAttrReq"
|
|
215
|
+
CONF_DIYS = "diys"
|
|
216
|
+
CONF_PRESETS = "presets"
|
|
217
|
+
CONF_FAV_PRESETS = "favoritePresets"
|
|
218
|
+
CONF_VERSION = "version"
|
|
219
|
+
CONF_LIGHT_SCRIPT_FLAGS = "lightScriptFlags"
|
|
220
|
+
CONF_PRIMITIVE = "primitive"
|
|
213
221
|
|
|
214
222
|
|
|
215
223
|
class Identity(StrEnum):
|
|
@@ -7,7 +7,7 @@ import time
|
|
|
7
7
|
import json
|
|
8
8
|
import asyncio
|
|
9
9
|
import logging
|
|
10
|
-
from typing import Any
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
11
|
|
|
12
12
|
from .aes_utils import aes_encrypt, aes_decrypt_to_json
|
|
13
13
|
from .models.device_client_model import (
|
|
@@ -32,6 +32,7 @@ from .const import (
|
|
|
32
32
|
CONF_ON_OFF,
|
|
33
33
|
CONF_DIMMING,
|
|
34
34
|
CONF_PASSWORD,
|
|
35
|
+
CONF_PRESETS,
|
|
35
36
|
CONF_PRODUCT,
|
|
36
37
|
CONF_PROPERTIES,
|
|
37
38
|
CONF_RGBW,
|
|
@@ -39,10 +40,14 @@ from .const import (
|
|
|
39
40
|
CONF_GET_DEV_ATTR_REQ,
|
|
40
41
|
CONF_SET_DEV_ATTR_REQ,
|
|
41
42
|
Identity,
|
|
43
|
+
CONF_EFFECT_MODE,
|
|
42
44
|
)
|
|
43
45
|
|
|
44
46
|
_LOGGER = logging.getLogger(__name__)
|
|
45
47
|
|
|
48
|
+
if TYPE_CHECKING:
|
|
49
|
+
from .client import AidotClient
|
|
50
|
+
|
|
46
51
|
|
|
47
52
|
class DeviceStatusData:
|
|
48
53
|
online: bool = False
|
|
@@ -51,6 +56,7 @@ class DeviceStatusData:
|
|
|
51
56
|
rgbw: tuple[int, int, int, int] = (255, 0, 0, 0)
|
|
52
57
|
cct: int = 2700
|
|
53
58
|
dimming: int = 100
|
|
59
|
+
effect: str = ""
|
|
54
60
|
|
|
55
61
|
def update(self, attr: DeviceAttr) -> None:
|
|
56
62
|
"""Update status from DeviceAttr model."""
|
|
@@ -61,6 +67,7 @@ class DeviceStatusData:
|
|
|
61
67
|
if attr.Dimming is not None:
|
|
62
68
|
self.dimming = int(attr.Dimming * 255 / 100)
|
|
63
69
|
if attr.RGBW is not None:
|
|
70
|
+
self.effect = ""
|
|
64
71
|
rgbw_value = attr.RGBW
|
|
65
72
|
# If RGBW is 0, set default red color (255, 0, 0, 0)
|
|
66
73
|
if rgbw_value == 0:
|
|
@@ -75,6 +82,7 @@ class DeviceStatusData:
|
|
|
75
82
|
w = rgbw & 0xFF
|
|
76
83
|
self.rgbw = (r, g, b, w)
|
|
77
84
|
if attr.CCT is not None:
|
|
85
|
+
self.effect = ""
|
|
78
86
|
self.cct = attr.CCT
|
|
79
87
|
|
|
80
88
|
|
|
@@ -89,6 +97,8 @@ class DeviceInformation:
|
|
|
89
97
|
model_id: str
|
|
90
98
|
name: str
|
|
91
99
|
hw_version: str
|
|
100
|
+
presets: dict[str, Any]
|
|
101
|
+
preset_names: list[str]
|
|
92
102
|
|
|
93
103
|
def __init__(self, device: dict[str, Any]) -> None:
|
|
94
104
|
self.dev_id = device.get(CONF_ID)
|
|
@@ -96,6 +106,8 @@ class DeviceInformation:
|
|
|
96
106
|
self.model_id = device.get(CONF_MODEL_ID)
|
|
97
107
|
self.name = device.get(CONF_NAME)
|
|
98
108
|
self.hw_version = device.get(CONF_HARDWARE_VERSION)
|
|
109
|
+
self.presets = device.get(CONF_PRESETS, {})
|
|
110
|
+
self.preset_names = list(self.presets)
|
|
99
111
|
if CONF_PRODUCT in device and CONF_SERVICE_MODULES in device[CONF_PRODUCT]:
|
|
100
112
|
for service in device[CONF_PRODUCT][CONF_SERVICE_MODULES]:
|
|
101
113
|
if service[CONF_IDENTITY] == Identity.RGBW:
|
|
@@ -124,7 +136,8 @@ class DeviceClient(object):
|
|
|
124
136
|
_ping_timer: Any = None
|
|
125
137
|
writer: Any = None
|
|
126
138
|
reader: Any = None
|
|
127
|
-
syncProperties = [CONF_ON_OFF, CONF_DIMMING, CONF_RGBW, CONF_CCT]
|
|
139
|
+
# syncProperties = [CONF_ON_OFF, CONF_DIMMING, CONF_RGBW, CONF_CCT, CONF_EFFECT_MODE]
|
|
140
|
+
syncProperties: Any = None
|
|
128
141
|
heart_time = 30
|
|
129
142
|
ping_data = PingRequest().to_dict()
|
|
130
143
|
_TAG: str = "DeviceClient"
|
|
@@ -137,10 +150,16 @@ class DeviceClient(object):
|
|
|
137
150
|
def connecting(self) -> bool:
|
|
138
151
|
return self._connecting
|
|
139
152
|
|
|
140
|
-
def __init__(
|
|
153
|
+
def __init__(
|
|
154
|
+
self,
|
|
155
|
+
device: dict[str, Any],
|
|
156
|
+
user_info: dict[str, Any],
|
|
157
|
+
client: "AidotClient",
|
|
158
|
+
) -> None:
|
|
141
159
|
self.ping_count = 0
|
|
142
160
|
self.status = DeviceStatusData()
|
|
143
161
|
self.info = DeviceInformation(device)
|
|
162
|
+
self.client = client
|
|
144
163
|
self.user_id = user_info.get(CONF_ID)
|
|
145
164
|
|
|
146
165
|
if CONF_AES_KEY in device:
|
|
@@ -154,11 +173,13 @@ class DeviceClient(object):
|
|
|
154
173
|
self.device_id = device.get(CONF_ID)
|
|
155
174
|
self._simpleVersion = device.get("simpleVersion")
|
|
156
175
|
self._TAG = f"{self.device_id}"
|
|
176
|
+
self.syncProperties = []
|
|
157
177
|
if self.info.model_id == "lk.WIFI-RGBWLight-D0006":
|
|
178
|
+
self.syncProperties = [CONF_ON_OFF, CONF_DIMMING, CONF_RGBW, CONF_CCT]
|
|
158
179
|
self.ping_data = None
|
|
159
180
|
self.heart_time = 10
|
|
160
181
|
|
|
161
|
-
_LOGGER.warning(f"{self._TAG}:{device}")
|
|
182
|
+
# _LOGGER.warning(f"{self._TAG}:{device}")
|
|
162
183
|
|
|
163
184
|
async def connect(self, ip_address) -> None:
|
|
164
185
|
_LOGGER.warning(f"{self._TAG}:connect device: {ip_address}")
|
|
@@ -291,8 +312,26 @@ class DeviceClient(object):
|
|
|
291
312
|
self.ascNumber = response.payload.ascNumber
|
|
292
313
|
if response.payload.attr:
|
|
293
314
|
self.status.update(response.payload.attr)
|
|
315
|
+
effect_name = self._get_effect_name_by_unique_id(
|
|
316
|
+
response.payload.attr.effectUniqueID
|
|
317
|
+
)
|
|
318
|
+
if effect_name is not None:
|
|
319
|
+
self.status.effect = effect_name
|
|
294
320
|
self._notify_status_update()
|
|
295
321
|
|
|
322
|
+
def _get_effect_name_by_unique_id(self, effect_unique_id: str | None) -> str | None:
|
|
323
|
+
"""Get effect display name by primitive effect id."""
|
|
324
|
+
if not effect_unique_id:
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
for effect_name, effect in self.info.presets.items():
|
|
328
|
+
if (
|
|
329
|
+
effect.primitiveEffectId == effect_unique_id
|
|
330
|
+
or effect.favoriteId == effect_unique_id
|
|
331
|
+
):
|
|
332
|
+
return effect_name
|
|
333
|
+
return None
|
|
334
|
+
|
|
296
335
|
def _schedule_ping(self):
|
|
297
336
|
loop = asyncio.get_running_loop()
|
|
298
337
|
loop.create_task(self.send_ping_action())
|
|
@@ -320,6 +359,17 @@ class DeviceClient(object):
|
|
|
320
359
|
final_rgbw = (rgbw[0] << 24) | (rgbw[1] << 16) | (rgbw[2] << 8) | rgbw[3]
|
|
321
360
|
await self.send_dev_attr({CONF_RGBW: ctypes.c_int32(final_rgbw).value})
|
|
322
361
|
|
|
362
|
+
async def async_set_effect(self, effect: str) -> None:
|
|
363
|
+
effect_item = self.info.presets.get(effect)
|
|
364
|
+
if effect_item is None:
|
|
365
|
+
raise ValueError(f"Unknown effect: {effect}")
|
|
366
|
+
if effect_item.primitiveEffectId is None:
|
|
367
|
+
raise ValueError(f"Effect has no primitiveEffectId: {effect}")
|
|
368
|
+
|
|
369
|
+
await self.client.async_execute_diff_command(
|
|
370
|
+
device_id=self.device_id, primitive=effect_item
|
|
371
|
+
)
|
|
372
|
+
|
|
323
373
|
async def async_set_cct(self, cct: int) -> None:
|
|
324
374
|
await self.send_dev_attr({CONF_CCT: cct})
|
|
325
375
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
APP_ID = "1383974540041977857"
|
|
4
4
|
|
|
5
5
|
# API URL template - use .format(region="us") to construct
|
|
6
|
-
API_URL_TEMPLATE = "https://prod-{region}-api.arnoo.com/
|
|
6
|
+
API_URL_TEMPLATE = "https://prod-{region}-api.arnoo.com/v35"
|
|
7
7
|
DEFAULT_REGION = "us"
|
|
8
8
|
|
|
9
9
|
PUBLIC_KEY_PEM = b"""
|
|
@@ -17,6 +17,9 @@ from .device_model import (
|
|
|
17
17
|
DeviceProperties,
|
|
18
18
|
DeviceProduct,
|
|
19
19
|
DeviceInformation,
|
|
20
|
+
FavoriteEffectMode,
|
|
21
|
+
FavoriteEffectPrimitive,
|
|
22
|
+
FavoriteEffectTags,
|
|
20
23
|
)
|
|
21
24
|
|
|
22
25
|
__all__ = [
|
|
@@ -34,4 +37,7 @@ __all__ = [
|
|
|
34
37
|
"DeviceProperties",
|
|
35
38
|
"DeviceProduct",
|
|
36
39
|
"DeviceInformation",
|
|
40
|
+
"FavoriteEffectMode",
|
|
41
|
+
"FavoriteEffectPrimitive",
|
|
42
|
+
"FavoriteEffectTags",
|
|
37
43
|
]
|
|
@@ -81,3 +81,60 @@ class DeviceInformation:
|
|
|
81
81
|
def to_dict(self) -> dict[str, Any]:
|
|
82
82
|
"""Convert to dictionary."""
|
|
83
83
|
return asdict(self)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class FavoriteEffectTags:
|
|
88
|
+
"""Favorite effect tags."""
|
|
89
|
+
|
|
90
|
+
advanced: str = None
|
|
91
|
+
directional: str = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class FavoriteEffectPrimitive:
|
|
96
|
+
"""Favorite primitive effect mode."""
|
|
97
|
+
|
|
98
|
+
primitiveEffectId: str = None
|
|
99
|
+
favoriteId: str = None
|
|
100
|
+
icon: str = None
|
|
101
|
+
libraryId: str = "aidot.preset"
|
|
102
|
+
name: str = None
|
|
103
|
+
tags: Optional[FavoriteEffectTags] = None
|
|
104
|
+
scriptSize: int = None
|
|
105
|
+
secondShareFlag: bool = None
|
|
106
|
+
changeFlag: bool = None
|
|
107
|
+
createTime: int = None
|
|
108
|
+
imageUrl: str = None
|
|
109
|
+
videoUrl: str = None
|
|
110
|
+
sharedUserName: str = None
|
|
111
|
+
sharedUserHeadImg: str = None
|
|
112
|
+
sharedTime: int = None
|
|
113
|
+
shareId: str = None
|
|
114
|
+
isOldParams: bool = False
|
|
115
|
+
|
|
116
|
+
def to_dict(self) -> dict[str, Any]:
|
|
117
|
+
"""Convert to dictionary."""
|
|
118
|
+
return asdict(self)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass
|
|
122
|
+
class FavoriteEffectMode:
|
|
123
|
+
"""Favorite effect mode response."""
|
|
124
|
+
|
|
125
|
+
data: list[Any] = field(default_factory=list)
|
|
126
|
+
primitive: list[FavoriteEffectPrimitive] = field(default_factory=list)
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def from_json(data: dict[str, Any]) -> "FavoriteEffectMode":
|
|
130
|
+
"""Create FavoriteEffectMode from JSON dict."""
|
|
131
|
+
return from_dict(
|
|
132
|
+
data_class=FavoriteEffectMode, data=data, config=Config(check_types=False)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def to_dict(self) -> dict[str, Any]:
|
|
136
|
+
"""Convert to dictionary."""
|
|
137
|
+
return asdict(self)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
EffectResp = FavoriteEffectMode
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|