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 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.56")
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 typing import Any
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__(self, device: dict[str, Any], user_info: dict[str, Any]) -> None:
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
- if self.info.model_id == "lk.WIFI-RGBWLight-D0006":
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
- _LOGGER.warning(f"{self._TAG}:{device}")
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/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"""
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
  ]
@@ -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,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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot
3
- Version: 0.3.56
3
+ Version: 0.3.59
4
4
  Summary: aidot control wifi lights
5
5
  Home-page: https://github.com/Aidot-Development-Team/python-aidot
6
6
  Author: aidotdev2024
@@ -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,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (83.0.0)
2
+ Generator: setuptools (84.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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,,