pyvesync 3.1.0__tar.gz → 3.1.2__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 (59) hide show
  1. {pyvesync-3.1.0/src/pyvesync.egg-info → pyvesync-3.1.2}/PKG-INFO +1 -1
  2. {pyvesync-3.1.0 → pyvesync-3.1.2}/pyproject.toml +1 -1
  3. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/auth.py +9 -0
  4. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/const.py +1 -0
  5. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/device_map.py +4 -4
  6. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/devices/vesynchumidifier.py +1 -0
  7. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/devices/vesyncpurifier.py +1 -0
  8. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/utils/errors.py +4 -4
  9. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/vesync.py +35 -1
  10. {pyvesync-3.1.0 → pyvesync-3.1.2/src/pyvesync.egg-info}/PKG-INFO +1 -1
  11. {pyvesync-3.1.0 → pyvesync-3.1.2}/LICENSE +0 -0
  12. {pyvesync-3.1.0 → pyvesync-3.1.2}/MANIFEST.in +0 -0
  13. {pyvesync-3.1.0 → pyvesync-3.1.2}/README.md +0 -0
  14. {pyvesync-3.1.0 → pyvesync-3.1.2}/requirements.txt +0 -0
  15. {pyvesync-3.1.0 → pyvesync-3.1.2}/setup.cfg +0 -0
  16. {pyvesync-3.1.0 → pyvesync-3.1.2}/setup.py +0 -0
  17. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/__init__.py +0 -0
  18. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/base_devices/__init__.py +0 -0
  19. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/base_devices/bulb_base.py +0 -0
  20. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/base_devices/fan_base.py +0 -0
  21. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/base_devices/fryer_base.py +0 -0
  22. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/base_devices/humidifier_base.py +0 -0
  23. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/base_devices/outlet_base.py +0 -0
  24. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/base_devices/purifier_base.py +0 -0
  25. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/base_devices/switch_base.py +0 -0
  26. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/base_devices/thermostat_base.py +0 -0
  27. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/base_devices/vesyncbasedevice.py +0 -0
  28. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/device_container.py +0 -0
  29. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/devices/__init__.py +0 -0
  30. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/devices/vesyncbulb.py +0 -0
  31. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/devices/vesyncfan.py +0 -0
  32. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/devices/vesynckitchen.py +0 -0
  33. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/devices/vesyncoutlet.py +0 -0
  34. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/devices/vesyncswitch.py +0 -0
  35. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/devices/vesyncthermostat.py +0 -0
  36. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/models/__init__.py +0 -0
  37. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/models/base_models.py +0 -0
  38. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/models/bulb_models.py +0 -0
  39. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/models/bypass_models.py +0 -0
  40. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/models/fan_models.py +0 -0
  41. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/models/fryer_models.py +0 -0
  42. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/models/home_models.py +0 -0
  43. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/models/humidifier_models.py +0 -0
  44. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/models/outlet_models.py +0 -0
  45. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/models/purifier_models.py +0 -0
  46. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/models/switch_models.py +0 -0
  47. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/models/thermostat_models.py +0 -0
  48. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/models/vesync_models.py +0 -0
  49. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/utils/__init__.py +0 -0
  50. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/utils/colors.py +0 -0
  51. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/utils/device_mixins.py +0 -0
  52. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/utils/enum_utils.py +0 -0
  53. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/utils/helpers.py +0 -0
  54. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/utils/logs.py +0 -0
  55. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync/vesynchome.py +0 -0
  56. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync.egg-info/SOURCES.txt +0 -0
  57. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync.egg-info/dependency_links.txt +0 -0
  58. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync.egg-info/requires.txt +0 -0
  59. {pyvesync-3.1.0 → pyvesync-3.1.2}/src/pyvesync.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyvesync
3
- Version: 3.1.0
3
+ Version: 3.1.2
4
4
  Summary: pyvesync is a library to manage Etekcity Devices, Cosori Air Fryers, and Levoit Air Purifiers run on the VeSync app.
5
5
  Author: Mark Perdue
6
6
  Author-email: Joe Trabulsy <webdjoe@gmail.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyvesync"
7
- version = "3.1.0"
7
+ version = "3.1.2"
8
8
  description = "pyvesync is a library to manage Etekcity Devices, Cosori Air Fryers, and Levoit Air Purifiers run on the VeSync app."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -145,6 +145,15 @@ class VeSyncAuth:
145
145
  self._country_code = country_code.upper()
146
146
  self._current_region = region
147
147
 
148
+ async def reauthenticate(self) -> bool:
149
+ """Re-authenticate using stored username and password.
150
+
151
+ Returns:
152
+ True if re-authentication successful, False otherwise
153
+ """
154
+ self.clear_credentials()
155
+ return await self.login()
156
+
148
157
  async def load_credentials_from_file(
149
158
  self, file_path: str | Path | None = None
150
159
  ) -> bool:
@@ -34,6 +34,7 @@ from uuid import uuid4
34
34
 
35
35
  from pyvesync.utils.enum_utils import IntEnumMixin
36
36
 
37
+ MAX_API_REAUTH_RETRIES = 3
37
38
  DEFAULT_LANGUAGE = 'en'
38
39
  API_BASE_URL = None # Global URL (non-EU regions): "https://smartapi.vesync.com"
39
40
  # If device is out of reach, the cloud api sends a timeout response after 7 seconds,
@@ -750,7 +750,7 @@ purifier_modules: list[PurifierMap] = [
750
750
  PurifierAutoPreference.QUIET,
751
751
  ],
752
752
  features=[PurifierFeatures.AIR_QUALITY],
753
- fan_levels=list(range(1, 5)),
753
+ fan_levels=list(range(1, 4)),
754
754
  device_alias='Core 300S',
755
755
  model_display='Core 300S',
756
756
  model_name='Core 300S',
@@ -790,7 +790,7 @@ purifier_modules: list[PurifierMap] = [
790
790
  ),
791
791
  PurifierMap(
792
792
  class_name='VeSyncAir131',
793
- dev_types=['LV-PUR131S', 'LV-RH131S'],
793
+ dev_types=['LV-PUR131S', 'LV-RH131S', 'LV-RH131S-WM'],
794
794
  modes=[PurifierModes.SLEEP, PurifierModes.MANUAL, PurifierModes.AUTO],
795
795
  features=[PurifierFeatures.AIR_QUALITY],
796
796
  fan_levels=list(range(1, 4)),
@@ -888,7 +888,7 @@ purifier_modules: list[PurifierMap] = [
888
888
  setup_entry='EL551S',
889
889
  ),
890
890
  PurifierMap(
891
- class_name='VeSyncAirBaseV2',
891
+ class_name='VeSyncAirSprout',
892
892
  dev_types=[
893
893
  'LAP-B851S-WEU',
894
894
  'LAP-B851S-WNA',
@@ -925,7 +925,7 @@ objects for purifier devices."""
925
925
  fan_modules: list[FanMap] = [
926
926
  FanMap(
927
927
  class_name='VeSyncTowerFan',
928
- dev_types=['LTF-F422S-KEU', 'LTF-F422S-WUSR', 'LTF-F422_WJP', 'LTF-F422S-WUS'],
928
+ dev_types=['LTF-F422S-KEU', 'LTF-F422S-WUSR', 'LTF-F422S-WJP', 'LTF-F422S-WUS'],
929
929
  modes=[
930
930
  FanModes.NORMAL,
931
931
  FanModes.TURBO,
@@ -665,6 +665,7 @@ class VeSyncHumid1000S(VeSyncHumid200300S):
665
665
  self.state.device_status = DeviceStatus.from_int(resp_model.powerSwitch)
666
666
  self.state.connection_status = ConnectionStatus.ONLINE
667
667
  self.state.mode = resp_model.workMode
668
+ self.state.humidity = resp_model.humidity
668
669
  self.state.auto_target_humidity = resp_model.targetHumidity
669
670
  self.state.mist_level = resp_model.mistLevel
670
671
  self.state.mist_virtual_level = resp_model.virtualLevel
@@ -123,6 +123,7 @@ class VeSyncAirBypass(BypassV2Mixin, VeSyncPurifier):
123
123
  result (InnerPurifierResult): Data model for inner result in purifier
124
124
  details response.
125
125
  """
126
+ self.state.connection_status = ConnectionStatus.ONLINE
126
127
  self.state.device_status = DeviceStatus.ON if result.enabled else DeviceStatus.OFF
127
128
  self.state.filter_life = result.filter_life or 0
128
129
  self.state.mode = result.mode
@@ -799,9 +799,11 @@ class VeSyncLoginError(VeSyncError):
799
799
  class VeSyncTokenError(VeSyncError):
800
800
  """Exception raised for VeSync API authentication errors."""
801
801
 
802
- def __init__(self) -> None:
802
+ def __init__(self, msg: str | None = None) -> None:
803
803
  """Initialize the exception with a message."""
804
- super().__init__('Token expired or invalid - please re-authenticate with login()')
804
+ super().__init__(
805
+ f'Token expired or invalid - {msg if msg else "Re-authentication required"}'
806
+ )
805
807
 
806
808
 
807
809
  class VeSyncServerError(VeSyncError):
@@ -857,8 +859,6 @@ def raise_api_errors(error_info: ResponseInfo) -> None:
857
859
  raise VeSyncRateLimitError
858
860
  case ErrorTypes.AUTHENTICATION:
859
861
  raise VeSyncLoginError(error_info.message)
860
- case ErrorTypes.TOKEN_ERROR:
861
- raise VeSyncTokenError
862
862
  case ErrorTypes.SERVER_ERROR:
863
863
  msg = (
864
864
  f'{error_info.message} - '
@@ -16,6 +16,7 @@ from pyvesync.auth import VeSyncAuth
16
16
  from pyvesync.const import (
17
17
  DEFAULT_REGION,
18
18
  DEFAULT_TZ,
19
+ MAX_API_REAUTH_RETRIES,
19
20
  REGION_API_MAP,
20
21
  STATUS_OK,
21
22
  )
@@ -34,6 +35,7 @@ from pyvesync.utils.errors import (
34
35
  VeSyncAPIStatusCodeError,
35
36
  VeSyncError,
36
37
  VeSyncServerError,
38
+ VeSyncTokenError,
37
39
  raise_api_errors,
38
40
  )
39
41
  from pyvesync.utils.helpers import Helpers
@@ -47,6 +49,7 @@ class VeSync: # pylint: disable=function-redefined
47
49
 
48
50
  __slots__ = (
49
51
  '__weakref__',
52
+ '_api_attempts',
50
53
  '_auth',
51
54
  '_close_session',
52
55
  '_debug',
@@ -130,6 +133,7 @@ class VeSync: # pylint: disable=function-redefined
130
133
  Object to store device state information
131
134
  """
132
135
  self.session = session
136
+ self._api_attempts = 0
133
137
  self._close_session = False
134
138
  self._debug = debug
135
139
  self.redact = redact
@@ -381,6 +385,7 @@ class VeSync: # pylint: disable=function-redefined
381
385
  VeSyncAPIResponseError: If API response is invalid.
382
386
  VeSyncServerError: If server returns an error.
383
387
  """
388
+ self.enabled = False
384
389
  success = await self._auth.login()
385
390
  if success:
386
391
  self.enabled = True
@@ -403,6 +408,9 @@ class VeSync: # pylint: disable=function-redefined
403
408
  async def update_all_devices(self) -> None:
404
409
  """Run `get_details()` for each device and update state."""
405
410
  logger.debug('Start updating the device details one by one')
411
+ if len(self._device_container) == 0:
412
+ logger.error('No devices to update')
413
+ return
406
414
  update_tasks: list[asyncio.Task] = [
407
415
  asyncio.create_task(device.update()) for device in self._device_container
408
416
  ]
@@ -410,7 +418,7 @@ class VeSync: # pylint: disable=function-redefined
410
418
  for task in done:
411
419
  exc = task.exception()
412
420
  if exc is not None and isinstance(exc, VeSyncError):
413
- logger.debug('Error updating device: %s', exc)
421
+ logger.error('Error updating device: %s', exc)
414
422
 
415
423
  async def __aenter__(self) -> Self:
416
424
  """Asynchronous context manager enter."""
@@ -424,6 +432,24 @@ class VeSync: # pylint: disable=function-redefined
424
432
  return
425
433
  logger.debug('Session not closed, exiting context manager')
426
434
 
435
+ async def _reauthenticate(self) -> bool:
436
+ """Re-authenticate using stored username and password.
437
+
438
+ Returns:
439
+ True if re-authentication successful, False otherwise
440
+ """
441
+ self.enabled = False
442
+ self._api_attempts += 1
443
+ if self._api_attempts >= MAX_API_REAUTH_RETRIES:
444
+ logger.error('Max API re-authentication attempts reached')
445
+ raise VeSyncTokenError
446
+ success = await self.auth.reauthenticate()
447
+ if success:
448
+ self.enabled = True
449
+ self._api_attempts = 0
450
+ return True
451
+ return await self.auth.reauthenticate()
452
+
427
453
  async def async_call_api(
428
454
  self,
429
455
  api: str,
@@ -460,6 +486,7 @@ class VeSync: # pylint: disable=function-redefined
460
486
  if self.session is None:
461
487
  self.session = ClientSession()
462
488
  self._close_session = True
489
+ self._api_attempts += 1
463
490
  response = None
464
491
  status_code = None
465
492
  if isinstance(json_object, DataClassORJSONMixin):
@@ -491,6 +518,13 @@ class VeSync: # pylint: disable=function-redefined
491
518
  resp_dict = Helpers.try_json_loads(resp_bytes)
492
519
  if isinstance(resp_dict, dict):
493
520
  error_info = ErrorCodes.get_error_info(resp_dict.get('code'))
521
+ if error_info.error_type == ErrorTypes.TOKEN_ERROR:
522
+ if await self._reauthenticate():
523
+ self.enabled = True
524
+ return await self.async_call_api(
525
+ api, method, json_object, headers
526
+ )
527
+ raise VeSyncTokenError
494
528
  if resp_dict.get('msg') is not None:
495
529
  error_info.message = f'{error_info.message} ({resp_dict["msg"]})'
496
530
  raise_api_errors(error_info)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyvesync
3
- Version: 3.1.0
3
+ Version: 3.1.2
4
4
  Summary: pyvesync is a library to manage Etekcity Devices, Cosori Air Fryers, and Levoit Air Purifiers run on the VeSync app.
5
5
  Author: Mark Perdue
6
6
  Author-email: Joe Trabulsy <webdjoe@gmail.com>
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes