python-aidot 0.3.56__py3-none-any.whl → 0.3.59__py3-none-any.whl
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.
- aidot/client.py +321 -2
- aidot/const.py +15 -0
- aidot/device_client.py +73 -6
- aidot/login_const.py +1 -1
- aidot/models/__init__.py +6 -0
- aidot/models/device_client_model.py +2 -0
- aidot/models/device_model.py +58 -0
- {python_aidot-0.3.56.dist-info → python_aidot-0.3.59.dist-info}/METADATA +1 -1
- python_aidot-0.3.59.dist-info/RECORD +17 -0
- {python_aidot-0.3.56.dist-info → python_aidot-0.3.59.dist-info}/WHEEL +1 -1
- python_aidot-0.3.56.dist-info/RECORD +0 -17
- {python_aidot-0.3.56.dist-info → python_aidot-0.3.59.dist-info}/licenses/LICENSE +0 -0
- {python_aidot-0.3.56.dist-info → python_aidot-0.3.59.dist-info}/top_level.txt +0 -0
aidot/client.py
CHANGED
|
@@ -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 (
|
|
@@ -22,6 +24,11 @@ from .const import (
|
|
|
22
24
|
CONF_CODE,
|
|
23
25
|
CONF_COUNTRY,
|
|
24
26
|
CONF_DEVICE_LIST,
|
|
27
|
+
CONF_DIYS,
|
|
28
|
+
CONF_EFFECTS,
|
|
29
|
+
CONF_EFFECT_SELECTION,
|
|
30
|
+
CONF_EFFECT_SOURCE,
|
|
31
|
+
CONF_FAV_PRESETS,
|
|
25
32
|
CONF_ID,
|
|
26
33
|
CONF_IPADDRESS,
|
|
27
34
|
CONF_PASSWORD,
|
|
@@ -38,6 +45,17 @@ from .const import (
|
|
|
38
45
|
CONF_IS_OWNER,
|
|
39
46
|
CONF_LOGIN_INFO,
|
|
40
47
|
ServerErrorCode,
|
|
48
|
+
CONF_MODEL_ID,
|
|
49
|
+
CONF_PROPERTIES,
|
|
50
|
+
CONF_LIGHT_SCRIPT_FLAGS,
|
|
51
|
+
CONF_PRESETS,
|
|
52
|
+
CONF_TYPE,
|
|
53
|
+
CONF_AES_KEY,
|
|
54
|
+
CONF_FIRMWARE_VERSION,
|
|
55
|
+
CONF_PRIMITIVE,
|
|
56
|
+
DEFAULT_EFFECT_SOURCE,
|
|
57
|
+
EFFECT_SOURCE_MANUAL,
|
|
58
|
+
EFFECT_SOURCE_RECOMMENDED,
|
|
41
59
|
)
|
|
42
60
|
|
|
43
61
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -66,9 +84,13 @@ class AidotClient:
|
|
|
66
84
|
username: str | None = None,
|
|
67
85
|
password: str | None = None,
|
|
68
86
|
token: dict | None = None,
|
|
87
|
+
options: dict[str, Any] | None = None,
|
|
88
|
+
effect_source: str = DEFAULT_EFFECT_SOURCE,
|
|
69
89
|
) -> None:
|
|
70
|
-
_LOGGER.info("Client Version: v0.3.
|
|
90
|
+
_LOGGER.info("Client Version: v0.3.59")
|
|
71
91
|
self.session = session
|
|
92
|
+
self.options = options.copy() if options is not None else {}
|
|
93
|
+
self.effect_source = self.options.get(CONF_EFFECT_SOURCE, effect_source)
|
|
72
94
|
self.username = username
|
|
73
95
|
self.password = password
|
|
74
96
|
self.country_code = country_code or DEFAULT_COUNTRY_CODE
|
|
@@ -77,6 +99,9 @@ class AidotClient:
|
|
|
77
99
|
self._base_url = API_URL_TEMPLATE.format(region=self._region)
|
|
78
100
|
self.login_info: dict[str, Any] = {}
|
|
79
101
|
self._device_clients = {}
|
|
102
|
+
self._effect_mode_params_cache: dict[
|
|
103
|
+
tuple[str, str, str, bool], dict[str, Any]
|
|
104
|
+
] = {}
|
|
80
105
|
self._discover: Discover | None = None
|
|
81
106
|
self._token_fresh_cb = None
|
|
82
107
|
for item in SUPPORTED_COUNTRYS:
|
|
@@ -222,6 +247,45 @@ class AidotClient:
|
|
|
222
247
|
raise AidotAuthFailed from err
|
|
223
248
|
raise
|
|
224
249
|
|
|
250
|
+
async def async_session_post(
|
|
251
|
+
self,
|
|
252
|
+
params: str,
|
|
253
|
+
data: Any,
|
|
254
|
+
headers: dict[str, str] | None = None,
|
|
255
|
+
) -> dict[str, Any]:
|
|
256
|
+
"""Post data to AiDot API."""
|
|
257
|
+
url = f"{self._base_url}{params}"
|
|
258
|
+
token = self.login_info[CONF_ACCESS_TOKEN]
|
|
259
|
+
if token is None:
|
|
260
|
+
raise AidotAuthFailed()
|
|
261
|
+
if headers is None:
|
|
262
|
+
headers = {
|
|
263
|
+
CONF_TERMINAL: "app",
|
|
264
|
+
CONF_TOKEN: token,
|
|
265
|
+
CONF_APP_ID: APP_ID,
|
|
266
|
+
}
|
|
267
|
+
response_data = {}
|
|
268
|
+
try:
|
|
269
|
+
response = await self.session.post(url, headers=headers, json=data)
|
|
270
|
+
response_data = await response.json()
|
|
271
|
+
response.raise_for_status()
|
|
272
|
+
return response_data
|
|
273
|
+
except aiohttp.ClientError as err:
|
|
274
|
+
_LOGGER.error("async_post ClientError: %s %s", err, response_data)
|
|
275
|
+
code = response_data.get(CONF_CODE)
|
|
276
|
+
if code == ServerErrorCode.TOKEN_EXPIRED:
|
|
277
|
+
try:
|
|
278
|
+
await self.async_refresh_token()
|
|
279
|
+
return await self.async_session_post(params, data)
|
|
280
|
+
except AidotAuthFailed as auth_err:
|
|
281
|
+
raise AidotAuthFailed from auth_err
|
|
282
|
+
elif (
|
|
283
|
+
code == ServerErrorCode.LOGIN_INVALID or code == 21027 or code == 21041
|
|
284
|
+
):
|
|
285
|
+
self.login_info[CONF_ACCESS_TOKEN] = None
|
|
286
|
+
raise AidotAuthFailed from err
|
|
287
|
+
raise
|
|
288
|
+
|
|
225
289
|
async def async_get_products(self, product_ids: str) -> list[dict[str, Any]]:
|
|
226
290
|
"""Get device list."""
|
|
227
291
|
params = f"/products/{product_ids}"
|
|
@@ -237,6 +301,253 @@ class AidotClient:
|
|
|
237
301
|
params = "/houses"
|
|
238
302
|
return await self.async_session_get(params)
|
|
239
303
|
|
|
304
|
+
async def async_get_diy_list(
|
|
305
|
+
self, device: dict[str, Any]
|
|
306
|
+
) -> list[FavoriteEffectPrimitive]:
|
|
307
|
+
"""Get favorite list."""
|
|
308
|
+
params = f"/devices/{device[CONF_ID]}/v4.0/favoriteEffectMode"
|
|
309
|
+
resp = await self.async_session_get(params)
|
|
310
|
+
return EffectResp.from_json(resp).primitive
|
|
311
|
+
|
|
312
|
+
async def async_get_fav_presets(
|
|
313
|
+
self, device: dict[str, Any]
|
|
314
|
+
) -> list[FavoriteEffectPrimitive]:
|
|
315
|
+
"""Get favorite list."""
|
|
316
|
+
# https://prod-us-api.arnoo.com/v35/devices/8a040e2233a243e7a07eb6a1b7a210a7/v4.0/favoriteEffectMode?libraryId=aidot.preset
|
|
317
|
+
|
|
318
|
+
params = (
|
|
319
|
+
f"/devices/{device[CONF_ID]}/v4.0/favoriteEffectMode?libraryId=aidot.preset"
|
|
320
|
+
)
|
|
321
|
+
resp = await self.async_session_get(params)
|
|
322
|
+
return EffectResp.from_json(resp).primitive
|
|
323
|
+
|
|
324
|
+
async def async_get_presets(
|
|
325
|
+
self, device: dict[str, Any]
|
|
326
|
+
) -> list[FavoriteEffectPrimitive]:
|
|
327
|
+
"""Get preset list."""
|
|
328
|
+
# https://prod-us-api.arnoo.com/v35/models/LK.light.A001855/v3.0/effectModes?libraryId=aidot.preset&version=3.30.09&lightScriptFlags=0002140503140500000062
|
|
329
|
+
|
|
330
|
+
params = (
|
|
331
|
+
f"/models/{device[CONF_MODEL_ID]}/v3.0/effectModes?libraryId=aidot.preset"
|
|
332
|
+
)
|
|
333
|
+
properties = device.get(CONF_PROPERTIES, {})
|
|
334
|
+
light_script_flags = properties.get(CONF_LIGHT_SCRIPT_FLAGS)
|
|
335
|
+
if light_script_flags:
|
|
336
|
+
params = (
|
|
337
|
+
f"{params}&version={device[CONF_FIRMWARE_VERSION]}"
|
|
338
|
+
f"&lightScriptFlags={light_script_flags}"
|
|
339
|
+
)
|
|
340
|
+
resp = await self.async_session_get(params)
|
|
341
|
+
return EffectResp.from_json(resp).primitive
|
|
342
|
+
|
|
343
|
+
async def async_get_effect_mode_params(
|
|
344
|
+
self,
|
|
345
|
+
device_id: str,
|
|
346
|
+
primitive: FavoriteEffectPrimitive,
|
|
347
|
+
) -> dict[str, Any]:
|
|
348
|
+
"""Get effect mode params."""
|
|
349
|
+
primitive_effect_id = primitive.primitiveEffectId
|
|
350
|
+
favorite_id = primitive.favoriteId
|
|
351
|
+
library_id = primitive.libraryId
|
|
352
|
+
if favorite_id is not None:
|
|
353
|
+
effect_query = f"favoriteIds={favorite_id}"
|
|
354
|
+
effect_cache_id = favorite_id
|
|
355
|
+
elif primitive_effect_id is not None:
|
|
356
|
+
effect_query = f"primitiveEffectIds={primitive_effect_id}"
|
|
357
|
+
effect_cache_id = primitive_effect_id
|
|
358
|
+
else:
|
|
359
|
+
raise ValueError(
|
|
360
|
+
"Effect primitive must have favoriteId or primitiveEffectId"
|
|
361
|
+
)
|
|
362
|
+
if library_id is None:
|
|
363
|
+
raise ValueError("Effect primitive must have libraryId")
|
|
364
|
+
|
|
365
|
+
cache_key = (device_id, effect_cache_id, library_id, primitive.isOldParams)
|
|
366
|
+
if cache_key in self._effect_mode_params_cache:
|
|
367
|
+
return self._effect_mode_params_cache[cache_key]
|
|
368
|
+
|
|
369
|
+
params = (
|
|
370
|
+
f"/devices/{device_id}/v3.0/effectModeParams"
|
|
371
|
+
f"?{effect_query}"
|
|
372
|
+
f"&isOldParams={primitive.isOldParams}"
|
|
373
|
+
f"&libraryId={library_id}"
|
|
374
|
+
)
|
|
375
|
+
resp = await self.async_session_get(params)
|
|
376
|
+
effect_primitives = resp.get(CONF_PRIMITIVE) or []
|
|
377
|
+
if not effect_primitives:
|
|
378
|
+
raise ValueError("Effect mode params response has no primitive")
|
|
379
|
+
|
|
380
|
+
effect_params = effect_primitives[0]
|
|
381
|
+
self._effect_mode_params_cache[cache_key] = effect_params
|
|
382
|
+
return effect_params
|
|
383
|
+
|
|
384
|
+
async def async_execute_diff_command(
|
|
385
|
+
self,
|
|
386
|
+
device_id: str,
|
|
387
|
+
primitive: FavoriteEffectPrimitive,
|
|
388
|
+
) -> dict[str, Any]:
|
|
389
|
+
"""Execute diff command."""
|
|
390
|
+
# effect_params = await self.async_get_effect_mode_params(device_id, primitive)
|
|
391
|
+
data = [
|
|
392
|
+
{
|
|
393
|
+
"devId": device_id,
|
|
394
|
+
"effectUniqueID": primitive.primitiveEffectId,
|
|
395
|
+
"action": "runLScript",
|
|
396
|
+
"in": [
|
|
397
|
+
{
|
|
398
|
+
"sessionId": random.randint(1, 2_147_483_647),
|
|
399
|
+
# "params": effect_params["params"],
|
|
400
|
+
}
|
|
401
|
+
],
|
|
402
|
+
}
|
|
403
|
+
]
|
|
404
|
+
return await self.async_session_post("/devices/execute/diffCommand", data)
|
|
405
|
+
|
|
406
|
+
async def async_get_all_effects(
|
|
407
|
+
self, device: dict[str, Any]
|
|
408
|
+
) -> dict[str, FavoriteEffectPrimitive]:
|
|
409
|
+
"""Get all effects list and store raw effect sources on the device."""
|
|
410
|
+
diy_list: list[FavoriteEffectPrimitive] = []
|
|
411
|
+
fav_preset: list[FavoriteEffectPrimitive] = []
|
|
412
|
+
preset_list: list[FavoriteEffectPrimitive] = []
|
|
413
|
+
|
|
414
|
+
try:
|
|
415
|
+
diy_list = await self.async_get_diy_list(device)
|
|
416
|
+
except Exception as err:
|
|
417
|
+
_LOGGER.warning(
|
|
418
|
+
"Failed to get DIY effects for %s: %s", device[CONF_ID], err
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
try:
|
|
422
|
+
fav_preset = await self.async_get_fav_presets(device)
|
|
423
|
+
except Exception as err:
|
|
424
|
+
_LOGGER.warning(
|
|
425
|
+
"Failed to get favorite preset effects for %s: %s",
|
|
426
|
+
device[CONF_ID],
|
|
427
|
+
err,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
try:
|
|
431
|
+
preset_list = await self.async_get_presets(device)
|
|
432
|
+
except Exception as err:
|
|
433
|
+
_LOGGER.warning(
|
|
434
|
+
"Failed to get preset effects for %s: %s", device[CONF_ID], err
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
fav_preset_ids = {
|
|
438
|
+
item.primitiveEffectId
|
|
439
|
+
for item in fav_preset
|
|
440
|
+
if item.primitiveEffectId is not None
|
|
441
|
+
}
|
|
442
|
+
preset_list = [
|
|
443
|
+
item for item in preset_list if item.primitiveEffectId not in fav_preset_ids
|
|
444
|
+
]
|
|
445
|
+
|
|
446
|
+
device[CONF_DIYS] = diy_list
|
|
447
|
+
device[CONF_FAV_PRESETS] = fav_preset
|
|
448
|
+
device[CONF_PRESETS] = preset_list
|
|
449
|
+
return self.get_filtered_effects(device)
|
|
450
|
+
|
|
451
|
+
def get_filtered_effects(
|
|
452
|
+
self, device: dict[str, Any]
|
|
453
|
+
) -> dict[str, FavoriteEffectPrimitive]:
|
|
454
|
+
"""Get final effects for Home Assistant from raw device effect sources."""
|
|
455
|
+
diy_list = device.get(CONF_DIYS, [])
|
|
456
|
+
fav_preset = device.get(CONF_FAV_PRESETS, [])
|
|
457
|
+
preset_list = device.get(CONF_PRESETS, [])
|
|
458
|
+
|
|
459
|
+
if self.effect_source == EFFECT_SOURCE_MANUAL:
|
|
460
|
+
return self._merge_effects_by_unique_name(
|
|
461
|
+
self._filter_manual_effects(device, diy_list),
|
|
462
|
+
self._filter_manual_effects(device, fav_preset),
|
|
463
|
+
self._filter_manual_effects(device, preset_list),
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
return self._merge_effects_by_unique_name(
|
|
467
|
+
diy_list,
|
|
468
|
+
fav_preset,
|
|
469
|
+
self._filter_preset_list(preset_list),
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
def get_all_cached_effects(
|
|
473
|
+
self, device: dict[str, Any]
|
|
474
|
+
) -> dict[str, FavoriteEffectPrimitive]:
|
|
475
|
+
"""Get all effects from cached raw device effect sources."""
|
|
476
|
+
return self._merge_effects_by_unique_name(
|
|
477
|
+
device.get(CONF_DIYS, []),
|
|
478
|
+
device.get(CONF_FAV_PRESETS, []),
|
|
479
|
+
device.get(CONF_PRESETS, []),
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
def _filter_manual_effects(
|
|
483
|
+
self, device: dict[str, Any], effect_list: list[FavoriteEffectPrimitive]
|
|
484
|
+
) -> list[FavoriteEffectPrimitive]:
|
|
485
|
+
"""Filter effect list by manual device selection."""
|
|
486
|
+
selected_ids = self._get_manual_effect_ids(device)
|
|
487
|
+
return [
|
|
488
|
+
effect
|
|
489
|
+
for effect in effect_list
|
|
490
|
+
if self._get_effect_ids(effect) & selected_ids
|
|
491
|
+
]
|
|
492
|
+
|
|
493
|
+
def _get_manual_effect_ids(self, device: dict[str, Any]) -> set[str]:
|
|
494
|
+
"""Get manually selected effect ids for a device."""
|
|
495
|
+
effect_selection = self.options.get(CONF_EFFECT_SELECTION, {})
|
|
496
|
+
selected_ids = effect_selection.get(device.get(CONF_ID), [])
|
|
497
|
+
return {effect_id for effect_id in selected_ids if effect_id}
|
|
498
|
+
|
|
499
|
+
@staticmethod
|
|
500
|
+
def _get_effect_ids(effect: FavoriteEffectPrimitive) -> set[str]:
|
|
501
|
+
"""Get selectable ids for an effect."""
|
|
502
|
+
return {
|
|
503
|
+
effect_id
|
|
504
|
+
for effect_id in (effect.primitiveEffectId, effect.favoriteId)
|
|
505
|
+
if effect_id
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
def _filter_preset_list(
|
|
509
|
+
self, preset_list: list[FavoriteEffectPrimitive]
|
|
510
|
+
) -> list[FavoriteEffectPrimitive]:
|
|
511
|
+
"""Filter preset effects by configured source."""
|
|
512
|
+
if self.effect_source != EFFECT_SOURCE_RECOMMENDED:
|
|
513
|
+
return preset_list
|
|
514
|
+
|
|
515
|
+
recommended = [
|
|
516
|
+
effect for effect in preset_list if self._is_recommended_effect(effect)
|
|
517
|
+
]
|
|
518
|
+
if recommended:
|
|
519
|
+
return recommended
|
|
520
|
+
return preset_list[:10]
|
|
521
|
+
|
|
522
|
+
@staticmethod
|
|
523
|
+
def _is_recommended_effect(effect: FavoriteEffectPrimitive) -> bool:
|
|
524
|
+
"""Return whether an effect should be included in recommended presets."""
|
|
525
|
+
return effect.tag not in (None, "") and effect.tag.strip() != "New"
|
|
526
|
+
|
|
527
|
+
@staticmethod
|
|
528
|
+
def _merge_effects_by_unique_name(
|
|
529
|
+
*effect_lists: list[FavoriteEffectPrimitive],
|
|
530
|
+
) -> dict[str, FavoriteEffectPrimitive]:
|
|
531
|
+
"""Merge effect lists into a dict with unique display names."""
|
|
532
|
+
effects: dict[str, FavoriteEffectPrimitive] = {}
|
|
533
|
+
name_counts: dict[str, int] = {}
|
|
534
|
+
|
|
535
|
+
for effect_list in effect_lists:
|
|
536
|
+
for effect in effect_list:
|
|
537
|
+
name = (
|
|
538
|
+
effect.name
|
|
539
|
+
or effect.primitiveEffectId
|
|
540
|
+
or effect.favoriteId
|
|
541
|
+
or "Effect"
|
|
542
|
+
)
|
|
543
|
+
name_counts[name] = name_counts.get(name, 0) + 1
|
|
544
|
+
unique_name = (
|
|
545
|
+
name if name_counts[name] == 1 else f"{name} ({name_counts[name]})"
|
|
546
|
+
)
|
|
547
|
+
effects[unique_name] = effect
|
|
548
|
+
|
|
549
|
+
return effects
|
|
550
|
+
|
|
240
551
|
async def async_get_all_device(self) -> dict[str, Any]:
|
|
241
552
|
final_device_list: list[dict[str, Any]] = []
|
|
242
553
|
try:
|
|
@@ -260,6 +571,14 @@ class AidotClient:
|
|
|
260
571
|
if device[CONF_PRODUCT_ID] == product[CONF_ID]:
|
|
261
572
|
device[CONF_PRODUCT] = product
|
|
262
573
|
|
|
574
|
+
for device in final_device_list:
|
|
575
|
+
if (
|
|
576
|
+
device[CONF_TYPE] == "light"
|
|
577
|
+
and CONF_AES_KEY in device
|
|
578
|
+
and device[CONF_AES_KEY][0] is not None
|
|
579
|
+
):
|
|
580
|
+
device[CONF_EFFECTS] = await self.async_get_all_effects(device)
|
|
581
|
+
|
|
263
582
|
except Exception as e:
|
|
264
583
|
raise e
|
|
265
584
|
return {CONF_DEVICE_LIST: final_device_list}
|
|
@@ -268,7 +587,7 @@ class AidotClient:
|
|
|
268
587
|
device_id = device.get(CONF_ID)
|
|
269
588
|
device_client: DeviceClient = self._device_clients.get(device_id)
|
|
270
589
|
if device_client is None:
|
|
271
|
-
device_client = DeviceClient(device, self.login_info)
|
|
590
|
+
device_client = DeviceClient(device, self.login_info, self)
|
|
272
591
|
self._device_clients[device_id] = device_client
|
|
273
592
|
if self._discover is not None:
|
|
274
593
|
ip = self._discover.discovered_device.get(device_id)
|
aidot/const.py
CHANGED
|
@@ -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,24 @@ 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_EFFECTS = "effects"
|
|
219
|
+
CONF_EFFECT_SOURCE = "effect_source"
|
|
220
|
+
CONF_EFFECT_SELECTION = "effect_selection"
|
|
221
|
+
EFFECT_SOURCE_ALL = "all"
|
|
222
|
+
EFFECT_SOURCE_RECOMMENDED = "recommended"
|
|
223
|
+
EFFECT_SOURCE_MANUAL = "manual"
|
|
224
|
+
DEFAULT_EFFECT_SOURCE = EFFECT_SOURCE_ALL
|
|
225
|
+
CONF_VERSION = "version"
|
|
226
|
+
CONF_LIGHT_SCRIPT_FLAGS = "lightScriptFlags"
|
|
227
|
+
CONF_PRIMITIVE = "primitive"
|
|
213
228
|
|
|
214
229
|
|
|
215
230
|
class Identity(StrEnum):
|
aidot/device_client.py
CHANGED
|
@@ -7,7 +7,8 @@ import time
|
|
|
7
7
|
import json
|
|
8
8
|
import asyncio
|
|
9
9
|
import logging
|
|
10
|
-
from
|
|
10
|
+
from ipaddress import ip_address
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
11
12
|
|
|
12
13
|
from .aes_utils import aes_encrypt, aes_decrypt_to_json
|
|
13
14
|
from .models.device_client_model import (
|
|
@@ -21,6 +22,7 @@ from .models.device_client_model import (
|
|
|
21
22
|
from .const import (
|
|
22
23
|
CONF_AES_KEY,
|
|
23
24
|
CONF_CCT,
|
|
25
|
+
CONF_EFFECTS,
|
|
24
26
|
CONF_HARDWARE_VERSION,
|
|
25
27
|
CONF_ID,
|
|
26
28
|
CONF_IDENTITY,
|
|
@@ -32,6 +34,7 @@ from .const import (
|
|
|
32
34
|
CONF_ON_OFF,
|
|
33
35
|
CONF_DIMMING,
|
|
34
36
|
CONF_PASSWORD,
|
|
37
|
+
CONF_PRESETS,
|
|
35
38
|
CONF_PRODUCT,
|
|
36
39
|
CONF_PROPERTIES,
|
|
37
40
|
CONF_RGBW,
|
|
@@ -39,10 +42,14 @@ from .const import (
|
|
|
39
42
|
CONF_GET_DEV_ATTR_REQ,
|
|
40
43
|
CONF_SET_DEV_ATTR_REQ,
|
|
41
44
|
Identity,
|
|
45
|
+
CONF_EFFECT_MODE,
|
|
42
46
|
)
|
|
43
47
|
|
|
44
48
|
_LOGGER = logging.getLogger(__name__)
|
|
45
49
|
|
|
50
|
+
if TYPE_CHECKING:
|
|
51
|
+
from .client import AidotClient
|
|
52
|
+
|
|
46
53
|
|
|
47
54
|
class DeviceStatusData:
|
|
48
55
|
online: bool = False
|
|
@@ -51,6 +58,7 @@ class DeviceStatusData:
|
|
|
51
58
|
rgbw: tuple[int, int, int, int] = (255, 0, 0, 0)
|
|
52
59
|
cct: int = 2700
|
|
53
60
|
dimming: int = 100
|
|
61
|
+
effect: str = ""
|
|
54
62
|
|
|
55
63
|
def update(self, attr: DeviceAttr) -> None:
|
|
56
64
|
"""Update status from DeviceAttr model."""
|
|
@@ -61,6 +69,7 @@ class DeviceStatusData:
|
|
|
61
69
|
if attr.Dimming is not None:
|
|
62
70
|
self.dimming = int(attr.Dimming * 255 / 100)
|
|
63
71
|
if attr.RGBW is not None:
|
|
72
|
+
self.effect = ""
|
|
64
73
|
rgbw_value = attr.RGBW
|
|
65
74
|
# If RGBW is 0, set default red color (255, 0, 0, 0)
|
|
66
75
|
if rgbw_value == 0:
|
|
@@ -75,6 +84,7 @@ class DeviceStatusData:
|
|
|
75
84
|
w = rgbw & 0xFF
|
|
76
85
|
self.rgbw = (r, g, b, w)
|
|
77
86
|
if attr.CCT is not None:
|
|
87
|
+
self.effect = ""
|
|
78
88
|
self.cct = attr.CCT
|
|
79
89
|
|
|
80
90
|
|
|
@@ -89,6 +99,8 @@ class DeviceInformation:
|
|
|
89
99
|
model_id: str
|
|
90
100
|
name: str
|
|
91
101
|
hw_version: str
|
|
102
|
+
presets: dict[str, Any]
|
|
103
|
+
preset_names: list[str]
|
|
92
104
|
|
|
93
105
|
def __init__(self, device: dict[str, Any]) -> None:
|
|
94
106
|
self.dev_id = device.get(CONF_ID)
|
|
@@ -96,6 +108,8 @@ class DeviceInformation:
|
|
|
96
108
|
self.model_id = device.get(CONF_MODEL_ID)
|
|
97
109
|
self.name = device.get(CONF_NAME)
|
|
98
110
|
self.hw_version = device.get(CONF_HARDWARE_VERSION)
|
|
111
|
+
self.presets = device.get(CONF_EFFECTS, device.get(CONF_PRESETS, {}))
|
|
112
|
+
self.preset_names = list(self.presets)
|
|
99
113
|
if CONF_PRODUCT in device and CONF_SERVICE_MODULES in device[CONF_PRODUCT]:
|
|
100
114
|
for service in device[CONF_PRODUCT][CONF_SERVICE_MODULES]:
|
|
101
115
|
if service[CONF_IDENTITY] == Identity.RGBW:
|
|
@@ -124,7 +138,8 @@ class DeviceClient(object):
|
|
|
124
138
|
_ping_timer: Any = None
|
|
125
139
|
writer: Any = None
|
|
126
140
|
reader: Any = None
|
|
127
|
-
syncProperties = [CONF_ON_OFF, CONF_DIMMING, CONF_RGBW, CONF_CCT]
|
|
141
|
+
# syncProperties = [CONF_ON_OFF, CONF_DIMMING, CONF_RGBW, CONF_CCT, CONF_EFFECT_MODE]
|
|
142
|
+
syncProperties: Any = None
|
|
128
143
|
heart_time = 30
|
|
129
144
|
ping_data = PingRequest().to_dict()
|
|
130
145
|
_TAG: str = "DeviceClient"
|
|
@@ -137,10 +152,16 @@ class DeviceClient(object):
|
|
|
137
152
|
def connecting(self) -> bool:
|
|
138
153
|
return self._connecting
|
|
139
154
|
|
|
140
|
-
def __init__(
|
|
155
|
+
def __init__(
|
|
156
|
+
self,
|
|
157
|
+
device: dict[str, Any],
|
|
158
|
+
user_info: dict[str, Any],
|
|
159
|
+
client: "AidotClient",
|
|
160
|
+
) -> None:
|
|
141
161
|
self.ping_count = 0
|
|
142
162
|
self.status = DeviceStatusData()
|
|
143
163
|
self.info = DeviceInformation(device)
|
|
164
|
+
self.client = client
|
|
144
165
|
self.user_id = user_info.get(CONF_ID)
|
|
145
166
|
|
|
146
167
|
if CONF_AES_KEY in device:
|
|
@@ -154,11 +175,28 @@ class DeviceClient(object):
|
|
|
154
175
|
self.device_id = device.get(CONF_ID)
|
|
155
176
|
self._simpleVersion = device.get("simpleVersion")
|
|
156
177
|
self._TAG = f"{self.device_id}"
|
|
157
|
-
|
|
178
|
+
self.syncProperties = []
|
|
179
|
+
if self.info.model_id == "lk.WIFI-RGBWLight-D0006": # 一代灯
|
|
180
|
+
self.syncProperties = [CONF_ON_OFF, CONF_DIMMING, CONF_RGBW, CONF_CCT]
|
|
158
181
|
self.ping_data = None
|
|
159
182
|
self.heart_time = 10
|
|
160
|
-
|
|
161
|
-
|
|
183
|
+
else:
|
|
184
|
+
properties = device.get(CONF_PROPERTIES)
|
|
185
|
+
if properties is not None:
|
|
186
|
+
ip_address = self._get_valid_ip(properties.get("ipAddress"))
|
|
187
|
+
self.update_ip_address(ip_address)
|
|
188
|
+
|
|
189
|
+
# _LOGGER.warning(f"{self._TAG}:{device}")
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
def _get_valid_ip(ip: str | None) -> str | None:
|
|
193
|
+
"""Return a valid IP address string."""
|
|
194
|
+
if not ip:
|
|
195
|
+
return None
|
|
196
|
+
try:
|
|
197
|
+
return str(ip_address(ip))
|
|
198
|
+
except ValueError:
|
|
199
|
+
return None
|
|
162
200
|
|
|
163
201
|
async def connect(self, ip_address) -> None:
|
|
164
202
|
_LOGGER.warning(f"{self._TAG}:connect device: {ip_address}")
|
|
@@ -291,8 +329,26 @@ class DeviceClient(object):
|
|
|
291
329
|
self.ascNumber = response.payload.ascNumber
|
|
292
330
|
if response.payload.attr:
|
|
293
331
|
self.status.update(response.payload.attr)
|
|
332
|
+
effect_name = self._get_effect_name_by_unique_id(
|
|
333
|
+
response.payload.attr.effectUniqueID
|
|
334
|
+
)
|
|
335
|
+
if effect_name is not None:
|
|
336
|
+
self.status.effect = effect_name
|
|
294
337
|
self._notify_status_update()
|
|
295
338
|
|
|
339
|
+
def _get_effect_name_by_unique_id(self, effect_unique_id: str | None) -> str | None:
|
|
340
|
+
"""Get effect display name by primitive effect id."""
|
|
341
|
+
if not effect_unique_id:
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
for effect_name, effect in self.info.presets.items():
|
|
345
|
+
if (
|
|
346
|
+
effect.primitiveEffectId == effect_unique_id
|
|
347
|
+
or effect.favoriteId == effect_unique_id
|
|
348
|
+
):
|
|
349
|
+
return effect_name
|
|
350
|
+
return None
|
|
351
|
+
|
|
296
352
|
def _schedule_ping(self):
|
|
297
353
|
loop = asyncio.get_running_loop()
|
|
298
354
|
loop.create_task(self.send_ping_action())
|
|
@@ -320,6 +376,17 @@ class DeviceClient(object):
|
|
|
320
376
|
final_rgbw = (rgbw[0] << 24) | (rgbw[1] << 16) | (rgbw[2] << 8) | rgbw[3]
|
|
321
377
|
await self.send_dev_attr({CONF_RGBW: ctypes.c_int32(final_rgbw).value})
|
|
322
378
|
|
|
379
|
+
async def async_set_effect(self, effect: str) -> None:
|
|
380
|
+
effect_item = self.info.presets.get(effect)
|
|
381
|
+
if effect_item is None:
|
|
382
|
+
raise ValueError(f"Unknown effect: {effect}")
|
|
383
|
+
if effect_item.primitiveEffectId is None:
|
|
384
|
+
raise ValueError(f"Effect has no primitiveEffectId: {effect}")
|
|
385
|
+
|
|
386
|
+
await self.client.async_execute_diff_command(
|
|
387
|
+
device_id=self.device_id, primitive=effect_item
|
|
388
|
+
)
|
|
389
|
+
|
|
323
390
|
async def async_set_cct(self, cct: int) -> None:
|
|
324
391
|
await self.send_dev_attr({CONF_CCT: cct})
|
|
325
392
|
|
aidot/login_const.py
CHANGED
|
@@ -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"""
|
aidot/models/__init__.py
CHANGED
|
@@ -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
|
]
|
aidot/models/device_model.py
CHANGED
|
@@ -81,3 +81,61 @@ 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
|
+
tag: str = None
|
|
104
|
+
tags: Optional[FavoriteEffectTags] = None
|
|
105
|
+
scriptSize: int = None
|
|
106
|
+
secondShareFlag: bool = None
|
|
107
|
+
changeFlag: bool = None
|
|
108
|
+
createTime: int = None
|
|
109
|
+
imageUrl: str = None
|
|
110
|
+
videoUrl: str = None
|
|
111
|
+
sharedUserName: str = None
|
|
112
|
+
sharedUserHeadImg: str = None
|
|
113
|
+
sharedTime: int = None
|
|
114
|
+
shareId: str = None
|
|
115
|
+
isOldParams: bool = False
|
|
116
|
+
|
|
117
|
+
def to_dict(self) -> dict[str, Any]:
|
|
118
|
+
"""Convert to dictionary."""
|
|
119
|
+
return asdict(self)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class FavoriteEffectMode:
|
|
124
|
+
"""Favorite effect mode response."""
|
|
125
|
+
|
|
126
|
+
data: list[Any] = field(default_factory=list)
|
|
127
|
+
primitive: list[FavoriteEffectPrimitive] = field(default_factory=list)
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def from_json(data: dict[str, Any]) -> "FavoriteEffectMode":
|
|
131
|
+
"""Create FavoriteEffectMode from JSON dict."""
|
|
132
|
+
return from_dict(
|
|
133
|
+
data_class=FavoriteEffectMode, data=data, config=Config(check_types=False)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def to_dict(self) -> dict[str, Any]:
|
|
137
|
+
"""Convert to dictionary."""
|
|
138
|
+
return asdict(self)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
EffectResp = FavoriteEffectMode
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
aidot/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
aidot/aes_utils.py,sha256=bGKuC8B6-d2rrNt5Mj7M9gQsaK8C2BDtTisHSnxjgA4,1554
|
|
3
|
+
aidot/client.py,sha256=evj6AXCcduUStAk-jlMdotY3_h6B-I-cSyddoH0VjO8,24026
|
|
4
|
+
aidot/const.py,sha256=Ly-OGX7Nth4ydy8Lq4JYSUg_RJ-NIoTxNkSmJnnVWP8,12957
|
|
5
|
+
aidot/device_client.py,sha256=SnDcRaptYznxHR86PAJfz-iiCVp3wwygo7HwSU_eCgk,17397
|
|
6
|
+
aidot/discover.py,sha256=1hGBPnPcYRvy72yty5ZMTth_mdZuAYgcSaXf6Z68jow,4944
|
|
7
|
+
aidot/exceptions.py,sha256=VJl_HOxpa9B_5vEA4ORNllDPSo28BBcMlV-mF8NiAk4,663
|
|
8
|
+
aidot/login_const.py,sha256=pjXF9ORsY18m_N8oR4WMfaCDBDvLzDXmmKaEmmup_BE,511
|
|
9
|
+
aidot/models/__init__.py,sha256=vdiTDsMQh7TAODvDS7IMAQp1CS7fK2C_f1CKyjLycqU,849
|
|
10
|
+
aidot/models/device_client_model.py,sha256=4Tjeq0L1NUp2Rf4mbGPkvMWE-lhLU8oHDVHkLxn-FRg,5116
|
|
11
|
+
aidot/models/device_model.py,sha256=630L_eezZWfDujpPQvseRpDC5csmKuOdcymrj8ao7HU,3425
|
|
12
|
+
aidot/models/discover_model.py,sha256=fQ77TbubypYFN12myHgXyXGF0yHEffHdD1HIpEwY4r8,2480
|
|
13
|
+
python_aidot-0.3.59.dist-info/licenses/LICENSE,sha256=5k6Ccd7T5u39Y8730J0qr1ahoaEkd1cahwEccuUZ2Dc,1079
|
|
14
|
+
python_aidot-0.3.59.dist-info/METADATA,sha256=M4HvkuUb8JH4enM8cpwuJnTAA8ICV53Xh9s5a8vGjxY,739
|
|
15
|
+
python_aidot-0.3.59.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
16
|
+
python_aidot-0.3.59.dist-info/top_level.txt,sha256=_doNL2OOnXeinm1X72eH2wz26wAkZwbM47KhNB6_QzI,6
|
|
17
|
+
python_aidot-0.3.59.dist-info/RECORD,,
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
aidot/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
aidot/aes_utils.py,sha256=bGKuC8B6-d2rrNt5Mj7M9gQsaK8C2BDtTisHSnxjgA4,1554
|
|
3
|
-
aidot/client.py,sha256=10JDvwaL-qyUVDHwIayBjp33kZQGGGqBoJQMisfz7TE,11902
|
|
4
|
-
aidot/const.py,sha256=101OdNMZ5MOWCaxkq3dL541UIY19k0bJRiT96yeay64,12456
|
|
5
|
-
aidot/device_client.py,sha256=gQGr44iCewWMiZYyeucilubIAaFZPll8wxKvFCtaCqU,15016
|
|
6
|
-
aidot/discover.py,sha256=1hGBPnPcYRvy72yty5ZMTth_mdZuAYgcSaXf6Z68jow,4944
|
|
7
|
-
aidot/exceptions.py,sha256=VJl_HOxpa9B_5vEA4ORNllDPSo28BBcMlV-mF8NiAk4,663
|
|
8
|
-
aidot/login_const.py,sha256=NihUuRWd7W9KDfGZQ6RDE9vdbBQE0qIbgUnETxQY93Q,511
|
|
9
|
-
aidot/models/__init__.py,sha256=PEKlm3-3Ke6K4UeAUD0AngOwNhPsjZeJuugg4l6w8tc,689
|
|
10
|
-
aidot/models/device_client_model.py,sha256=Ge8I2TnQRT-gavGwwh9-TZ9QFl0UcAQdhrRB-xpMHdI,5058
|
|
11
|
-
aidot/models/device_model.py,sha256=HF4VPxym3ziVcLiaGZUbACIJqmlHF1QY9_qrb3tNYY4,1980
|
|
12
|
-
aidot/models/discover_model.py,sha256=fQ77TbubypYFN12myHgXyXGF0yHEffHdD1HIpEwY4r8,2480
|
|
13
|
-
python_aidot-0.3.56.dist-info/licenses/LICENSE,sha256=5k6Ccd7T5u39Y8730J0qr1ahoaEkd1cahwEccuUZ2Dc,1079
|
|
14
|
-
python_aidot-0.3.56.dist-info/METADATA,sha256=U7jwtPIsKOew0VMFLOyFvPJus2GwWrQzN4tIDCaVwJw,739
|
|
15
|
-
python_aidot-0.3.56.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
16
|
-
python_aidot-0.3.56.dist-info/top_level.txt,sha256=_doNL2OOnXeinm1X72eH2wz26wAkZwbM47KhNB6_QzI,6
|
|
17
|
-
python_aidot-0.3.56.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|