python-aidot 0.3.56__tar.gz → 0.3.57__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.
Files changed (23) hide show
  1. {python_aidot-0.3.56/python_aidot.egg-info → python_aidot-0.3.57}/PKG-INFO +1 -1
  2. {python_aidot-0.3.56 → python_aidot-0.3.57}/aidot/client.py +229 -2
  3. {python_aidot-0.3.56 → python_aidot-0.3.57}/aidot/const.py +8 -0
  4. {python_aidot-0.3.56 → python_aidot-0.3.57}/aidot/device_client.py +54 -4
  5. {python_aidot-0.3.56 → python_aidot-0.3.57}/aidot/login_const.py +1 -1
  6. {python_aidot-0.3.56 → python_aidot-0.3.57}/aidot/models/__init__.py +6 -0
  7. {python_aidot-0.3.56 → python_aidot-0.3.57}/aidot/models/device_client_model.py +2 -0
  8. {python_aidot-0.3.56 → python_aidot-0.3.57}/aidot/models/device_model.py +57 -0
  9. {python_aidot-0.3.56 → python_aidot-0.3.57/python_aidot.egg-info}/PKG-INFO +1 -1
  10. {python_aidot-0.3.56 → python_aidot-0.3.57}/setup.py +1 -1
  11. {python_aidot-0.3.56 → python_aidot-0.3.57}/LICENSE +0 -0
  12. {python_aidot-0.3.56 → python_aidot-0.3.57}/README.md +0 -0
  13. {python_aidot-0.3.56 → python_aidot-0.3.57}/aidot/__init__.py +0 -0
  14. {python_aidot-0.3.56 → python_aidot-0.3.57}/aidot/aes_utils.py +0 -0
  15. {python_aidot-0.3.56 → python_aidot-0.3.57}/aidot/discover.py +0 -0
  16. {python_aidot-0.3.56 → python_aidot-0.3.57}/aidot/exceptions.py +0 -0
  17. {python_aidot-0.3.56 → python_aidot-0.3.57}/aidot/models/discover_model.py +0 -0
  18. {python_aidot-0.3.56 → python_aidot-0.3.57}/python_aidot.egg-info/SOURCES.txt +0 -0
  19. {python_aidot-0.3.56 → python_aidot-0.3.57}/python_aidot.egg-info/dependency_links.txt +0 -0
  20. {python_aidot-0.3.56 → python_aidot-0.3.57}/python_aidot.egg-info/requires.txt +0 -0
  21. {python_aidot-0.3.56 → python_aidot-0.3.57}/python_aidot.egg-info/top_level.txt +0 -0
  22. {python_aidot-0.3.56 → python_aidot-0.3.57}/setup.cfg +0 -0
  23. {python_aidot-0.3.56 → python_aidot-0.3.57}/tests/test_client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot
3
- Version: 0.3.56
3
+ Version: 0.3.57
4
4
  Summary: aidot control wifi lights
5
5
  Home-page: https://github.com/Aidot-Development-Team/python-aidot
6
6
  Author: aidotdev2024
@@ -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.56")
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__(self, device: dict[str, Any], user_info: dict[str, Any]) -> None:
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/v17"
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
  ]
@@ -92,6 +92,8 @@ class DeviceAttr:
92
92
  Dimming: int = None
93
93
  RGBW: int = None
94
94
  CCT: int = None
95
+ effectUniqueID: str = None
96
+ EffectMode: str = None
95
97
 
96
98
 
97
99
  @dataclass
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot
3
- Version: 0.3.56
3
+ Version: 0.3.57
4
4
  Summary: aidot control wifi lights
5
5
  Home-page: https://github.com/Aidot-Development-Team/python-aidot
6
6
  Author: aidotdev2024
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setuptools.setup(
7
7
  name="python-aidot",
8
- version="0.3.56",
8
+ version="0.3.57",
9
9
  author="aidotdev2024",
10
10
  url="https://github.com/Aidot-Development-Team/python-aidot",
11
11
  description="aidot control wifi lights",
File without changes
File without changes
File without changes