pyvesync 3.0.0b2__tar.gz → 3.0.0b4__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.
- {pyvesync-3.0.0b2/src/pyvesync.egg-info → pyvesync-3.0.0b4}/PKG-INFO +18 -1
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/README.md +17 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/pyproject.toml +1 -1
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/base_devices/purifier_base.py +1 -1
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/base_devices/switch_base.py +1 -1
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/const.py +14 -5
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/devices/vesyncbulb.py +1 -1
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/devices/vesyncfan.py +14 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/devices/vesynchumidifier.py +2 -2
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/devices/vesyncoutlet.py +2 -2
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/devices/vesyncswitch.py +2 -4
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/models/base_models.py +4 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/models/vesync_models.py +98 -32
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/utils/errors.py +6 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/utils/logs.py +21 -12
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/vesync.py +135 -27
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4/src/pyvesync.egg-info}/PKG-INFO +18 -1
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/LICENSE +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/MANIFEST.in +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/requirements.txt +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/setup.cfg +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/setup.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/__init__.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/base_devices/__init__.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/base_devices/bulb_base.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/base_devices/fan_base.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/base_devices/fryer_base.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/base_devices/humidifier_base.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/base_devices/outlet_base.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/base_devices/thermostat_base.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/base_devices/vesyncbasedevice.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/device_container.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/device_map.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/devices/__init__.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/devices/vesynckitchen.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/devices/vesyncpurifier.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/devices/vesyncthermostat.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/models/__init__.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/models/bulb_models.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/models/bypass_models.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/models/fan_models.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/models/fryer_models.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/models/home_models.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/models/humidifier_models.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/models/outlet_models.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/models/purifier_models.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/models/switch_models.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/models/thermostat_models.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/utils/__init__.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/utils/colors.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/utils/device_mixins.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/utils/enum_utils.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/utils/helpers.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync/vesynchome.py +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync.egg-info/SOURCES.txt +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync.egg-info/dependency_links.txt +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/src/pyvesync.egg-info/requires.txt +0 -0
- {pyvesync-3.0.0b2 → pyvesync-3.0.0b4}/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.0.
|
|
3
|
+
Version: 3.0.0b4
|
|
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>
|
|
@@ -525,3 +525,20 @@ After:
|
|
|
525
525
|
All [contributions](CONTRIBUTING.md) are welcome.
|
|
526
526
|
|
|
527
527
|
This project is licensed under [MIT](LICENSE).
|
|
528
|
+
|
|
529
|
+
## Contributors
|
|
530
|
+
|
|
531
|
+
This is an open source project and cannot exist without the contributions of its community. Thank you to all the contributors who have helped make this project better!
|
|
532
|
+
|
|
533
|
+
A special thanks for helping with V3 go live:
|
|
534
|
+
|
|
535
|
+
| [<img src="https://github.com/cdninja.png" width="60px;"/><br /><sub><a href="https://github.com/cdninja">cdninja</a></sub>](https://github.com/cdninja) | [<img src="https://github.com/sapuseven.png" width="60px;"/><br /><sub><a href="https://github.com/sapuseven">sapuseven</a></sub>](https://github.com/sapuseven) | [<img src="https://github.com/sdrapha.png" width="60px;"/><br /><sub><a href="https://github.com/sdrapha">sdrapha</a></sub>](https://github.com/sdrapha) |
|
|
536
|
+
| :------- | :------- | :------- |
|
|
537
|
+
|
|
538
|
+
And to all of those that contributed to the project:
|
|
539
|
+
|
|
540
|
+
<a href="https://github.com/webdjoe/pyvesync/graphs/contributors">
|
|
541
|
+
<img src="https://contrib.rocks/image?repo=webdjoe/pyvesync" />
|
|
542
|
+
</a>
|
|
543
|
+
|
|
544
|
+
Made with [contrib.rocks](https://contrib.rocks).
|
|
@@ -494,3 +494,20 @@ After:
|
|
|
494
494
|
All [contributions](CONTRIBUTING.md) are welcome.
|
|
495
495
|
|
|
496
496
|
This project is licensed under [MIT](LICENSE).
|
|
497
|
+
|
|
498
|
+
## Contributors
|
|
499
|
+
|
|
500
|
+
This is an open source project and cannot exist without the contributions of its community. Thank you to all the contributors who have helped make this project better!
|
|
501
|
+
|
|
502
|
+
A special thanks for helping with V3 go live:
|
|
503
|
+
|
|
504
|
+
| [<img src="https://github.com/cdninja.png" width="60px;"/><br /><sub><a href="https://github.com/cdninja">cdninja</a></sub>](https://github.com/cdninja) | [<img src="https://github.com/sapuseven.png" width="60px;"/><br /><sub><a href="https://github.com/sapuseven">sapuseven</a></sub>](https://github.com/sapuseven) | [<img src="https://github.com/sdrapha.png" width="60px;"/><br /><sub><a href="https://github.com/sdrapha">sdrapha</a></sub>](https://github.com/sdrapha) |
|
|
505
|
+
| :------- | :------- | :------- |
|
|
506
|
+
|
|
507
|
+
And to all of those that contributed to the project:
|
|
508
|
+
|
|
509
|
+
<a href="https://github.com/webdjoe/pyvesync/graphs/contributors">
|
|
510
|
+
<img src="https://contrib.rocks/image?repo=webdjoe/pyvesync" />
|
|
511
|
+
</a>
|
|
512
|
+
|
|
513
|
+
Made with [contrib.rocks](https://contrib.rocks).
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pyvesync"
|
|
7
|
-
version = "3.0.
|
|
7
|
+
version = "3.0.0b4"
|
|
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"
|
|
@@ -125,7 +125,7 @@ class PurifierState(DeviceState):
|
|
|
125
125
|
@air_quality_level.setter
|
|
126
126
|
def air_quality_level(self, value: int | None) -> None:
|
|
127
127
|
"""Set air quality level."""
|
|
128
|
-
if isinstance(value,
|
|
128
|
+
if isinstance(value, int):
|
|
129
129
|
self._air_quality_level = AirQualityLevel.from_int(value)
|
|
130
130
|
|
|
131
131
|
def set_air_quality_level(self, value: int | str | None) -> None:
|
|
@@ -102,7 +102,7 @@ class VeSyncSwitch(VeSyncBaseToggleDevice):
|
|
|
102
102
|
"""Etekcity Switch Base Class.
|
|
103
103
|
|
|
104
104
|
Abstract Base Class for Etekcity Switch Devices, inheriting from
|
|
105
|
-
pyvesync.vesyncbasedevice.VeSyncBaseDevice. Should not be instantiated directly,
|
|
105
|
+
pyvesync.base_devices.vesyncbasedevice.VeSyncBaseDevice. Should not be instantiated directly,
|
|
106
106
|
subclassed by VeSyncWallSwitch and VeSyncDimmerSwitch.
|
|
107
107
|
|
|
108
108
|
Attributes:
|
|
@@ -4,7 +4,8 @@ All device states and information are defined by Enums in this module.
|
|
|
4
4
|
|
|
5
5
|
Attributes:
|
|
6
6
|
DEFAULT_LANGUAGE (str): Default language for the VeSync app.
|
|
7
|
-
API_BASE_URL (str): Base URL for the VeSync API.
|
|
7
|
+
API_BASE_URL (str): Base URL for the VeSync API. If not specified,
|
|
8
|
+
a region-specific API URL is automatically selected.
|
|
8
9
|
API_TIMEOUT (int): Timeout for API requests.
|
|
9
10
|
USER_AGENT (str): User agent for API requests.
|
|
10
11
|
DEFAULT_TZ (str): Default timezone for VeSync devices, updated by API after
|
|
@@ -12,6 +13,8 @@ Attributes:
|
|
|
12
13
|
DEFAULT_REGION (str): Default region for VeSync devices,
|
|
13
14
|
updated by API when retrieving devices.
|
|
14
15
|
APP_VERSION (str): Version of the VeSync app.
|
|
16
|
+
APP_ID (str): ID of the app. VeSync uses a random 8-letter string, but any non-empty
|
|
17
|
+
string works.
|
|
15
18
|
PHONE_BRAND (str): Brand of the phone used to login to the VeSync app.
|
|
16
19
|
PHONE_OS (str): Operating system of the phone used to login to the VeSync app.
|
|
17
20
|
MOBILE_ID (str): Unique identifier for the phone used to login to the VeSync app.
|
|
@@ -20,24 +23,29 @@ Attributes:
|
|
|
20
23
|
BYPASS_HEADER_UA (str): Bypass header user agent
|
|
21
24
|
TERMINAL_ID (str): Unique identifier for new API calls
|
|
22
25
|
"""
|
|
23
|
-
from random import randint
|
|
26
|
+
from random import randint, choices
|
|
24
27
|
from uuid import uuid4
|
|
28
|
+
import string
|
|
25
29
|
from enum import StrEnum, IntEnum, Enum
|
|
26
30
|
from types import MappingProxyType
|
|
27
31
|
from pyvesync.utils.enum_utils import IntEnumMixin
|
|
28
32
|
|
|
29
33
|
DEFAULT_LANGUAGE = "en"
|
|
30
|
-
API_BASE_URL = "https://smartapi.vesync.com"
|
|
34
|
+
API_BASE_URL = None # Global URL (non-EU regions): "https://smartapi.vesync.com"
|
|
31
35
|
# If device is out of reach, the cloud api sends a timeout response after 7 seconds,
|
|
32
36
|
# using 8 here so there is time enough to catch that message
|
|
37
|
+
API_BASE_URL_US = "https://smartapi.vesync.com"
|
|
38
|
+
API_BASE_URL_EU = "https://smartapi.vesync.eu"
|
|
39
|
+
NON_EU_REGIONS = ['US', 'CA', 'MX', 'JP']
|
|
33
40
|
API_TIMEOUT = 8
|
|
34
41
|
USER_AGENT = (
|
|
35
42
|
"VeSync/3.2.39 (com.etekcity.vesyncPlatform; build:5; iOS 15.5.0) Alamofire/5.2.1"
|
|
36
43
|
)
|
|
37
44
|
DEFAULT_TZ = "America/New_York"
|
|
38
45
|
DEFAULT_REGION = "US"
|
|
39
|
-
APP_VERSION = "5.
|
|
40
|
-
|
|
46
|
+
APP_VERSION = "5.6.60"
|
|
47
|
+
APP_ID = ''.join(choices(string.ascii_lowercase + string.digits, k=8)) # noqa: S311
|
|
48
|
+
PHONE_BRAND = "pyvesync"
|
|
41
49
|
PHONE_OS = "Android"
|
|
42
50
|
# MOBILE_ID = "1234567890123456"
|
|
43
51
|
MOBILE_ID = str(randint(1000000000000000, 9999999999999999)) # noqa: S311
|
|
@@ -45,6 +53,7 @@ USER_TYPE = "1"
|
|
|
45
53
|
BYPASS_APP_V = f"VeSync {APP_VERSION}"
|
|
46
54
|
BYPASS_HEADER_UA = "okhttp/3.12.1"
|
|
47
55
|
TERMINAL_ID = '2' + str(uuid4()).replace('-', '')
|
|
56
|
+
CLIENT_TYPE = "vesyncApp"
|
|
48
57
|
|
|
49
58
|
|
|
50
59
|
# Generic Constants
|
|
@@ -15,7 +15,7 @@ from __future__ import annotations
|
|
|
15
15
|
import logging
|
|
16
16
|
from collections.abc import Mapping
|
|
17
17
|
from typing import TYPE_CHECKING
|
|
18
|
-
from
|
|
18
|
+
from typing_extensions import deprecated
|
|
19
19
|
from pyvesync.base_devices import VeSyncBulb
|
|
20
20
|
from pyvesync.const import DeviceStatus, ConnectionStatus
|
|
21
21
|
from pyvesync.models.base_models import DefaultValues
|
|
@@ -125,6 +125,20 @@ class VeSyncTowerFan(BypassV2Mixin, VeSyncFanBase):
|
|
|
125
125
|
self.state.connection_status = ConnectionStatus.ONLINE
|
|
126
126
|
return True
|
|
127
127
|
|
|
128
|
+
async def toggle_switch(self, toggle: bool | None = None) -> bool:
|
|
129
|
+
payload_data = {
|
|
130
|
+
'powerSwitch': int(toggle),
|
|
131
|
+
'switchIdx': 0,
|
|
132
|
+
}
|
|
133
|
+
r_dict = await self.call_bypassv2_api("setSwitch", payload_data)
|
|
134
|
+
r = Helpers.process_dev_response(logger, "toggle_switch", self, r_dict)
|
|
135
|
+
if r is None:
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
self.state.device_status = DeviceStatus.from_bool(toggle)
|
|
139
|
+
self.state.connection_status = ConnectionStatus.ONLINE
|
|
140
|
+
return True
|
|
141
|
+
|
|
128
142
|
async def toggle_oscillation(self, toggle: bool) -> bool:
|
|
129
143
|
payload_data = {
|
|
130
144
|
"oscillationSwitch": int(toggle)
|
|
@@ -246,7 +246,7 @@ class VeSyncHumid200300S(BypassV2Mixin, VeSyncHumidifier):
|
|
|
246
246
|
self.device_name, self.device_type)
|
|
247
247
|
return False
|
|
248
248
|
|
|
249
|
-
if Validators.validate_zero_to_hundred(brightness):
|
|
249
|
+
if not Validators.validate_zero_to_hundred(brightness):
|
|
250
250
|
logger.debug("Brightness value must be set between 0 and 100")
|
|
251
251
|
return False
|
|
252
252
|
|
|
@@ -666,7 +666,7 @@ class VeSyncHumid1000S(VeSyncHumid200300S):
|
|
|
666
666
|
|
|
667
667
|
self._set_state(r_model)
|
|
668
668
|
|
|
669
|
-
@deprecated("Use
|
|
669
|
+
@deprecated("Use toggle_display() instead.")
|
|
670
670
|
async def set_display(self, toggle: bool) -> bool:
|
|
671
671
|
"""Toggle display on/off.
|
|
672
672
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
import logging
|
|
5
5
|
from typing import TYPE_CHECKING
|
|
6
|
-
from
|
|
6
|
+
from typing_extensions import deprecated
|
|
7
7
|
|
|
8
8
|
from pyvesync.utils.helpers import Helpers, Timer
|
|
9
9
|
from pyvesync.utils.logs import LibraryLogger
|
|
@@ -620,7 +620,7 @@ class VeSyncOutdoorPlug(BypassV1Mixin, VeSyncOutlet):
|
|
|
620
620
|
if int(self.sub_device_no) == int(outlet.subDeviceNo):
|
|
621
621
|
self.state.device_status = outlet.subDeviceStatus
|
|
622
622
|
|
|
623
|
-
@deprecated(
|
|
623
|
+
@deprecated("Use toggle_switch(toggle: bool | None) instead")
|
|
624
624
|
async def toggle(self, status: str) -> bool:
|
|
625
625
|
"""Deprecated, use toggle_switch() instead."""
|
|
626
626
|
toggle = status != DeviceStatus.ON
|
|
@@ -25,7 +25,7 @@ from dataclasses import asdict
|
|
|
25
25
|
import logging
|
|
26
26
|
# from abc import ABCMeta, abstractmethod
|
|
27
27
|
from typing import TYPE_CHECKING
|
|
28
|
-
from
|
|
28
|
+
from typing_extensions import deprecated
|
|
29
29
|
|
|
30
30
|
from pyvesync.base_devices.switch_base import VeSyncSwitch
|
|
31
31
|
from pyvesync.utils.colors import Color
|
|
@@ -265,9 +265,7 @@ class VeSyncDimmerSwitch(BypassV1Mixin, VeSyncSwitch):
|
|
|
265
265
|
self.state.indicator_status = result.indicatorlightStatus
|
|
266
266
|
self.state.device_status = result.deviceStatus
|
|
267
267
|
|
|
268
|
-
@deprecated(
|
|
269
|
-
reason="switch_toggle() deprecated, use toggle_switch(toggle: bool | None = None)"
|
|
270
|
-
)
|
|
268
|
+
@deprecated("switch_toggle() deprecated, use toggle_switch(toggle: bool | None = None)")
|
|
271
269
|
async def switch_toggle(self, status: str) -> bool:
|
|
272
270
|
"""Toggle switch status."""
|
|
273
271
|
return await self.toggle_switch(status == DeviceStatus.ON)
|
|
@@ -27,6 +27,7 @@ from mashumaro.config import BaseConfig
|
|
|
27
27
|
from pyvesync.const import (
|
|
28
28
|
USER_TYPE,
|
|
29
29
|
APP_VERSION,
|
|
30
|
+
APP_ID,
|
|
30
31
|
PHONE_BRAND,
|
|
31
32
|
PHONE_OS,
|
|
32
33
|
MOBILE_ID,
|
|
@@ -35,6 +36,7 @@ from pyvesync.const import (
|
|
|
35
36
|
DEFAULT_TZ,
|
|
36
37
|
BYPASS_HEADER_UA,
|
|
37
38
|
TERMINAL_ID,
|
|
39
|
+
CLIENT_TYPE
|
|
38
40
|
)
|
|
39
41
|
|
|
40
42
|
|
|
@@ -86,6 +88,8 @@ class DefaultValues:
|
|
|
86
88
|
_call_number: ClassVar[int] = 0
|
|
87
89
|
userType: str = USER_TYPE
|
|
88
90
|
appVersion: str = APP_VERSION
|
|
91
|
+
clientType: str = CLIENT_TYPE
|
|
92
|
+
appId: str = APP_ID
|
|
89
93
|
phoneBrand: str = PHONE_BRAND
|
|
90
94
|
phoneOS: str = PHONE_OS
|
|
91
95
|
mobileId: str = MOBILE_ID
|
|
@@ -23,8 +23,8 @@ from pyvesync.models.base_models import (
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
@dataclass
|
|
26
|
-
class
|
|
27
|
-
"""Request model for login."""
|
|
26
|
+
class RequestAuthModel(RequestBaseModel):
|
|
27
|
+
"""Request model for requesting auth token (used for login)."""
|
|
28
28
|
# Arguments to set
|
|
29
29
|
email: str
|
|
30
30
|
method: str
|
|
@@ -32,21 +32,22 @@ class RequestLoginModel(RequestBaseModel):
|
|
|
32
32
|
# default values
|
|
33
33
|
acceptLanguage: str = DefaultValues.acceptLanguage
|
|
34
34
|
accountID: str = ''
|
|
35
|
+
authProtocolType: str = "generic"
|
|
35
36
|
clientInfo: str = DefaultValues.phoneBrand
|
|
36
|
-
clientType: str =
|
|
37
|
+
clientType: str = DefaultValues.clientType
|
|
37
38
|
clientVersion: str = f"VeSync {DefaultValues.appVersion}"
|
|
38
39
|
debugMode: bool = False
|
|
39
40
|
osInfo: str = DefaultValues.phoneOS
|
|
40
41
|
terminalId: str = DefaultValues.terminalId
|
|
41
42
|
timeZone: str = DefaultValues.timeZone
|
|
42
43
|
token: str = ''
|
|
43
|
-
userCountryCode: str =
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
userCountryCode: str = DefaultValues.userCountryCode
|
|
45
|
+
appID: str = DefaultValues.appId
|
|
46
|
+
sourceAppID: str = DefaultValues.appId
|
|
46
47
|
traceId: str = field(default_factory=DefaultValues.newTraceId)
|
|
47
48
|
|
|
48
49
|
def __post_init__(self) -> None:
|
|
49
|
-
"""
|
|
50
|
+
"""Hash the password field."""
|
|
50
51
|
self.password = self.hash_password(self.password)
|
|
51
52
|
|
|
52
53
|
@staticmethod
|
|
@@ -55,6 +56,50 @@ class RequestLoginModel(RequestBaseModel):
|
|
|
55
56
|
return hashlib.md5(string.encode('utf-8')).hexdigest() # noqa: S324
|
|
56
57
|
|
|
57
58
|
|
|
59
|
+
@dataclass
|
|
60
|
+
class IntRespAuthResultModel(ResponseBaseModel):
|
|
61
|
+
"""Model for the 'result' field in auth response with authorizeCode and account ID.
|
|
62
|
+
|
|
63
|
+
This class is referenced by the `ResponseAuthModel` class.
|
|
64
|
+
"""
|
|
65
|
+
accountID: str
|
|
66
|
+
authorizeCode: str
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class RequestLoginModel(RequestBaseModel):
|
|
71
|
+
"""Request model for login."""
|
|
72
|
+
# Arguments to set
|
|
73
|
+
method: str
|
|
74
|
+
authorizeCode: str | None
|
|
75
|
+
# default values
|
|
76
|
+
acceptLanguage: str = DefaultValues.acceptLanguage
|
|
77
|
+
accountID: str = ''
|
|
78
|
+
clientInfo: str = DefaultValues.phoneBrand
|
|
79
|
+
clientType: str = DefaultValues.clientType
|
|
80
|
+
clientVersion: str = f"VeSync {DefaultValues.appVersion}"
|
|
81
|
+
debugMode: bool = False
|
|
82
|
+
emailSubscriptions: bool = False
|
|
83
|
+
osInfo: str = DefaultValues.phoneOS
|
|
84
|
+
terminalId: str = DefaultValues.terminalId
|
|
85
|
+
timeZone: str = DefaultValues.timeZone
|
|
86
|
+
token: str = ""
|
|
87
|
+
bizToken: str | None = None
|
|
88
|
+
regionChange: str | None = None
|
|
89
|
+
userCountryCode: str = DefaultValues.userCountryCode
|
|
90
|
+
traceId: str = field(default_factory=DefaultValues.newTraceId)
|
|
91
|
+
|
|
92
|
+
def __post_serialize__(self, d: dict[Any, Any]) -> dict[Any, Any]:
|
|
93
|
+
"""Remove null keys."""
|
|
94
|
+
if d['regionChange'] is None:
|
|
95
|
+
d.pop('regionChange')
|
|
96
|
+
if d['authorizeCode'] is None:
|
|
97
|
+
d.pop('authorizeCode')
|
|
98
|
+
if d['bizToken'] is None:
|
|
99
|
+
d.pop('bizToken')
|
|
100
|
+
return d
|
|
101
|
+
|
|
102
|
+
|
|
58
103
|
@dataclass
|
|
59
104
|
class IntRespLoginResultModel(ResponseBaseModel):
|
|
60
105
|
"""Model for the 'result' field in login response containing token and account ID.
|
|
@@ -65,6 +110,40 @@ class IntRespLoginResultModel(ResponseBaseModel):
|
|
|
65
110
|
acceptLanguage: str
|
|
66
111
|
countryCode: str
|
|
67
112
|
token: str
|
|
113
|
+
bizToken: str = ''
|
|
114
|
+
currentRegion: str = ''
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# @dataclass
|
|
118
|
+
# class ResponseAuthModel(ResponseCodeModel):
|
|
119
|
+
# """Model for the auth response.
|
|
120
|
+
|
|
121
|
+
# Inherits from `BaseResultModel`. The `BaseResultModel` class provides the
|
|
122
|
+
# defaults "code" and "msg" fields for the response.
|
|
123
|
+
|
|
124
|
+
# Attributes:
|
|
125
|
+
# result: ResponseAuthResultModel
|
|
126
|
+
# The inner model for the 'result' field in the auth response.
|
|
127
|
+
|
|
128
|
+
# Examples:
|
|
129
|
+
# ```python
|
|
130
|
+
# a = {
|
|
131
|
+
# "code": 0,
|
|
132
|
+
# "msg": "success",
|
|
133
|
+
# "stacktrace": null,
|
|
134
|
+
# "module": null,
|
|
135
|
+
# "traceId": "123456",
|
|
136
|
+
# "result": {
|
|
137
|
+
# "accountID": "123456",
|
|
138
|
+
# "authorizeCode": "abcdef1234567890"
|
|
139
|
+
# }
|
|
140
|
+
# }
|
|
141
|
+
# b = ResponseAuthModel.from_dict(a)
|
|
142
|
+
# account_id = b.result.accountId
|
|
143
|
+
# authorizeCode = b.result.authorizeCode
|
|
144
|
+
# ```
|
|
145
|
+
# """
|
|
146
|
+
# result: IntRespAuthResultModel
|
|
68
147
|
|
|
69
148
|
|
|
70
149
|
@dataclass
|
|
@@ -91,14 +170,14 @@ class ResponseLoginModel(ResponseCodeModel):
|
|
|
91
170
|
"acceptLanguage": "en",
|
|
92
171
|
"countryCode": "US",
|
|
93
172
|
"token": "abcdef1234567890"
|
|
94
|
-
|
|
173
|
+
}
|
|
95
174
|
}
|
|
96
175
|
b = ResponseLoginModel.from_dict(a)
|
|
97
176
|
account_id = b.result.accountId
|
|
98
177
|
token = b.result.token
|
|
99
178
|
```
|
|
100
179
|
"""
|
|
101
|
-
result: IntRespLoginResultModel
|
|
180
|
+
result: IntRespLoginResultModel | IntRespAuthResultModel
|
|
102
181
|
|
|
103
182
|
|
|
104
183
|
@dataclass
|
|
@@ -127,7 +206,7 @@ def _flatten_device_prop(d: dict[str, Any]) -> dict[str, Any]:
|
|
|
127
206
|
d['connectionStatus'] = device_prop['connectionStatus']
|
|
128
207
|
if device_prop.get('wifiMac') is not None:
|
|
129
208
|
d['macID'] = device_prop['wifiMac']
|
|
130
|
-
|
|
209
|
+
d.pop("deviceProp", None)
|
|
131
210
|
return d
|
|
132
211
|
|
|
133
212
|
|
|
@@ -151,38 +230,25 @@ class ResponseDeviceDetailsModel(ResponseBaseModel):
|
|
|
151
230
|
type: str
|
|
152
231
|
uuid: str | None
|
|
153
232
|
configModule: str
|
|
154
|
-
macID: str
|
|
155
|
-
mode: str
|
|
156
|
-
speed: str | None
|
|
157
|
-
currentFirmVersion: str
|
|
158
|
-
subDeviceType: str | None
|
|
159
|
-
subDeviceList:
|
|
160
|
-
extension:
|
|
233
|
+
macID: str = ""
|
|
234
|
+
mode: str = ""
|
|
235
|
+
speed: str | None = None
|
|
236
|
+
currentFirmVersion: str | None = None
|
|
237
|
+
subDeviceType: str | None = None
|
|
238
|
+
subDeviceList: str | None = None
|
|
239
|
+
extension: InternalDeviceListExtension | None = None
|
|
161
240
|
subDeviceNo: int | None = None
|
|
162
241
|
deviceStatus: str = "off"
|
|
163
242
|
connectionStatus: str = "offline"
|
|
164
243
|
productType: str | None = None
|
|
165
|
-
# deviceProp: InitVar[None | InternalDevicePropModel] = None
|
|
166
|
-
|
|
167
|
-
# def __post_init__(self, deviceProp: None | IntRespDevicePropModel) -> None:
|
|
168
|
-
# """Set productType based on deviceType."""
|
|
169
|
-
# if deviceProp is not None:
|
|
170
|
-
# if deviceProp.powerSwitch == 1:
|
|
171
|
-
# self.deviceStatus = "on"
|
|
172
|
-
# elif deviceProp.powerSwitch == 0:
|
|
173
|
-
# self.deviceStatus = "off"
|
|
174
|
-
# if isinstance(deviceProp.connectionStatus, str) and \
|
|
175
|
-
# self.connectionStatus != deviceProp.connectionStatus:
|
|
176
|
-
# self.connectionStatus = deviceProp.connectionStatus
|
|
177
244
|
|
|
178
245
|
@classmethod
|
|
179
246
|
def __pre_deserialize__(cls, d: dict[Any, Any]) -> dict[Any, Any]:
|
|
180
247
|
"""Perform device_list pre-deserialization processes.
|
|
181
248
|
|
|
182
249
|
This performs the following:
|
|
183
|
-
- Flattens the deviceProp field into deviceStatus and
|
|
184
|
-
- Sets
|
|
185
|
-
- Sets cid to uuid or macID if null
|
|
250
|
+
- Flattens the `deviceProp` field into `deviceStatus`, `connectionStatus` and `macID`
|
|
251
|
+
- Sets `cid` to `uuid` or `macID` if null
|
|
186
252
|
"""
|
|
187
253
|
super().__pre_deserialize__(d)
|
|
188
254
|
d = _flatten_device_prop(d)
|
|
@@ -97,6 +97,7 @@ class ErrorTypes(StrEnum):
|
|
|
97
97
|
UNKNOWN_ERROR = "unknown_error"
|
|
98
98
|
TOKEN_ERROR = "token_error"
|
|
99
99
|
BAD_RESPONSE = "bad_response"
|
|
100
|
+
CROSS_REGION = "cross_region"
|
|
100
101
|
|
|
101
102
|
|
|
102
103
|
class ErrorCodes:
|
|
@@ -135,6 +136,11 @@ class ErrorCodes:
|
|
|
135
136
|
|
|
136
137
|
errors: MappingProxyType[str, ResponseInfo] = MappingProxyType(
|
|
137
138
|
{
|
|
139
|
+
"-11260022": ResponseInfo(
|
|
140
|
+
"CROSS_REGION_ERROR",
|
|
141
|
+
ErrorTypes.CROSS_REGION,
|
|
142
|
+
"Cross region error",
|
|
143
|
+
),
|
|
138
144
|
"11": ResponseInfo(
|
|
139
145
|
"DEVICE_OFFLINE",
|
|
140
146
|
ErrorTypes.DEVICE_OFFLINE,
|
|
@@ -100,6 +100,7 @@ class LibraryLogger:
|
|
|
100
100
|
their values with '##_REDACTED_##'. The keys that are redacted include:
|
|
101
101
|
|
|
102
102
|
- token
|
|
103
|
+
- authorizeCode
|
|
103
104
|
- password
|
|
104
105
|
- email
|
|
105
106
|
- tk
|
|
@@ -130,6 +131,7 @@ class LibraryLogger:
|
|
|
130
131
|
r'(?<=uuid":\s")|'
|
|
131
132
|
r'(?<=cid":\s")|'
|
|
132
133
|
r"(?<=token\s)|"
|
|
134
|
+
r"(?<=authorizeCode\s)|"
|
|
133
135
|
r"(?<=account_id\s))"
|
|
134
136
|
r'[^"\s]+'
|
|
135
137
|
),
|
|
@@ -182,7 +184,9 @@ class LibraryLogger:
|
|
|
182
184
|
|
|
183
185
|
@staticmethod
|
|
184
186
|
def configure_logger(
|
|
185
|
-
level: str | int = logging.INFO,
|
|
187
|
+
level: str | int = logging.INFO,
|
|
188
|
+
file_name: str | Path | None = None,
|
|
189
|
+
std_out: bool = True,
|
|
186
190
|
) -> None:
|
|
187
191
|
"""Configure pyvesync library logger with a specific log level.
|
|
188
192
|
|
|
@@ -191,6 +195,7 @@ class LibraryLogger:
|
|
|
191
195
|
in form of enum `logging.DEBUG` or string `DEBUG`.
|
|
192
196
|
file_name (str | None): The name of the file to log to. If None,
|
|
193
197
|
logs will only be printed to the console.
|
|
198
|
+
std_out (bool): If True, logs will be printed to standard output.
|
|
194
199
|
|
|
195
200
|
Note:
|
|
196
201
|
This method configures the pyvesync base logger and prevents
|
|
@@ -203,18 +208,22 @@ class LibraryLogger:
|
|
|
203
208
|
if root_logger.handlers:
|
|
204
209
|
for handler in root_logger.handlers:
|
|
205
210
|
root_logger.removeHandler(handler)
|
|
206
|
-
|
|
211
|
+
|
|
207
212
|
formatter = logging.Formatter(
|
|
208
213
|
fmt="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
|
|
209
214
|
datefmt="%Y-%m-%d %H:%M:%S",
|
|
210
215
|
)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
216
|
+
if std_out is True:
|
|
217
|
+
str_handler = logging.StreamHandler()
|
|
218
|
+
str_handler.setFormatter(formatter)
|
|
219
|
+
root_logger.addHandler(str_handler)
|
|
220
|
+
if isinstance(file_name, str):
|
|
221
|
+
file_name_path = Path(file_name).resolve()
|
|
222
|
+
elif isinstance(file_name, Path):
|
|
223
|
+
file_name_path = file_name.resolve()
|
|
224
|
+
else:
|
|
225
|
+
file_name_path = None
|
|
226
|
+
if file_name_path:
|
|
218
227
|
file_handler = logging.FileHandler(file_name_path)
|
|
219
228
|
file_handler.setFormatter(formatter)
|
|
220
229
|
root_logger.addHandler(file_handler)
|
|
@@ -274,11 +283,11 @@ class LibraryLogger:
|
|
|
274
283
|
if dif:
|
|
275
284
|
msg += "\n\n-------------------------------------"
|
|
276
285
|
msg += "\n Expected Fields:"
|
|
277
|
-
msg += f"\n({
|
|
286
|
+
msg += f"\n({', '.join(field_f)})"
|
|
278
287
|
msg += "\n Response Fields:"
|
|
279
|
-
msg += f"\n({
|
|
288
|
+
msg += f"\n({', '.join(resp_f)})"
|
|
280
289
|
msg += "\n Missing Fields:"
|
|
281
|
-
msg += f"\n({
|
|
290
|
+
msg += f"\n({', '.join(dif)})"
|
|
282
291
|
|
|
283
292
|
msg += "\n\n Full Response:"
|
|
284
293
|
msg += f"\n{orjson.dumps(resp_dict, option=orjson.OPT_INDENT_2).decode('utf-8')}"
|
|
@@ -3,21 +3,31 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
import logging
|
|
5
5
|
import asyncio
|
|
6
|
+
from pathlib import Path
|
|
6
7
|
from typing import Self
|
|
7
8
|
from dataclasses import fields, MISSING
|
|
8
9
|
from aiohttp import ClientSession
|
|
9
10
|
from aiohttp.client_exceptions import ClientResponseError
|
|
10
11
|
from mashumaro.mixins.orjson import DataClassORJSONMixin
|
|
12
|
+
from mashumaro.exceptions import UnserializableDataError, MissingField
|
|
11
13
|
|
|
12
14
|
from pyvesync.utils.helpers import Helpers
|
|
13
|
-
from pyvesync.const import
|
|
15
|
+
from pyvesync.const import (
|
|
16
|
+
API_BASE_URL_US,
|
|
17
|
+
API_BASE_URL_EU,
|
|
18
|
+
DEFAULT_REGION,
|
|
19
|
+
NON_EU_REGIONS,
|
|
20
|
+
)
|
|
14
21
|
from pyvesync.device_container import DeviceContainer, DeviceContainerInstance
|
|
15
22
|
from pyvesync.utils.logs import LibraryLogger
|
|
16
23
|
from pyvesync.models.vesync_models import (
|
|
17
24
|
RequestDeviceListModel,
|
|
18
25
|
ResponseDeviceListModel,
|
|
26
|
+
RequestAuthModel,
|
|
27
|
+
IntRespAuthResultModel,
|
|
19
28
|
RequestLoginModel,
|
|
20
29
|
ResponseLoginModel,
|
|
30
|
+
IntRespLoginResultModel,
|
|
21
31
|
RequestFirmwareModel,
|
|
22
32
|
ResponseFirmwareModel,
|
|
23
33
|
FirmwareDeviceItemModel
|
|
@@ -67,8 +77,11 @@ class VeSync: # pylint: disable=function-redefined
|
|
|
67
77
|
def __init__(self,
|
|
68
78
|
username: str,
|
|
69
79
|
password: str,
|
|
80
|
+
country_code: str = DEFAULT_REGION,
|
|
70
81
|
session: ClientSession | None = None,
|
|
71
|
-
time_zone: str = DEFAULT_TZ
|
|
82
|
+
time_zone: str = DEFAULT_TZ,
|
|
83
|
+
debug: bool = False,
|
|
84
|
+
redact: bool = True) -> None:
|
|
72
85
|
"""Initialize VeSync Manager.
|
|
73
86
|
|
|
74
87
|
This class is used as the manager for all VeSync objects, all methods and
|
|
@@ -83,6 +96,11 @@ class VeSync: # pylint: disable=function-redefined
|
|
|
83
96
|
Args:
|
|
84
97
|
username (str): VeSync account username (usually email address)
|
|
85
98
|
password (str): VeSync account password
|
|
99
|
+
country_code (str): VeSync account country in ISO 3166 Alpha-2 format.
|
|
100
|
+
By default, the account region is detected automatically at the login step
|
|
101
|
+
If your account country is different from the default `US`,
|
|
102
|
+
a second login attempt may be necessary - in this case
|
|
103
|
+
you should specify the country directly to speed up the login process.
|
|
86
104
|
session (ClientSession): aiohttp client session for
|
|
87
105
|
API calls, by default None
|
|
88
106
|
time_zone (str): Time zone for device from IANA database, by default
|
|
@@ -122,15 +140,13 @@ class VeSync: # pylint: disable=function-redefined
|
|
|
122
140
|
"""
|
|
123
141
|
self.session = session
|
|
124
142
|
self._close_session = False
|
|
125
|
-
self._debug =
|
|
126
|
-
self.
|
|
127
|
-
if self._redact:
|
|
128
|
-
self.redact = self.redact
|
|
143
|
+
self._debug = debug
|
|
144
|
+
self.redact = redact
|
|
129
145
|
self.username: str = username
|
|
130
146
|
self.password: str = password
|
|
131
147
|
self._token: str | None = None
|
|
132
148
|
self._account_id: str | None = None
|
|
133
|
-
self.country_code: str =
|
|
149
|
+
self.country_code: str = country_code.upper()
|
|
134
150
|
self._verbose: bool = False
|
|
135
151
|
self.time_zone: str = time_zone
|
|
136
152
|
self.language: str = 'en'
|
|
@@ -207,10 +223,15 @@ class VeSync: # pylint: disable=function-redefined
|
|
|
207
223
|
LibraryLogger.shouldredact = False
|
|
208
224
|
self._redact = new_flag
|
|
209
225
|
|
|
210
|
-
def log_to_file(self, filename: str) -> None:
|
|
211
|
-
"""Log to file and enable debug logging.
|
|
226
|
+
def log_to_file(self, filename: str | Path, std_out: bool = True) -> None:
|
|
227
|
+
"""Log to file and enable debug logging.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
filename (str | Path): The name of the file to log to.
|
|
231
|
+
std_out (bool): If False, logs will not print to std out.
|
|
232
|
+
"""
|
|
212
233
|
self.debug = True
|
|
213
|
-
LibraryLogger.configure_logger(logging.DEBUG, file_name=filename)
|
|
234
|
+
LibraryLogger.configure_logger(logging.DEBUG, file_name=filename, std_out=std_out)
|
|
214
235
|
logger.debug('Logging to file: %s', filename)
|
|
215
236
|
|
|
216
237
|
def process_devices(self, dev_list_resp: ResponseDeviceListModel) -> bool:
|
|
@@ -300,38 +321,112 @@ class VeSync: # pylint: disable=function-redefined
|
|
|
300
321
|
if not isinstance(self.username, str) or len(self.username) == 0 \
|
|
301
322
|
or not isinstance(self.password, str) or len(self.password) == 0:
|
|
302
323
|
raise VesyncLoginError('Username and password must be specified')
|
|
303
|
-
|
|
324
|
+
request_auth = RequestAuthModel(
|
|
304
325
|
email=self.username,
|
|
305
|
-
method='
|
|
326
|
+
method='authByPWDOrOTM',
|
|
306
327
|
password=self.password,
|
|
307
328
|
)
|
|
308
329
|
resp_dict, _ = await self.async_call_api(
|
|
309
|
-
'/
|
|
310
|
-
json_object=
|
|
330
|
+
'/globalPlatform/api/accountAuth/v1/authByPWDOrOTM', 'post',
|
|
331
|
+
json_object=request_auth
|
|
311
332
|
)
|
|
312
333
|
if resp_dict is None:
|
|
313
|
-
raise VeSyncAPIResponseError('Error receiving response to
|
|
334
|
+
raise VeSyncAPIResponseError('Error receiving response to auth request')
|
|
314
335
|
if resp_dict.get('code') == 0:
|
|
315
336
|
try:
|
|
316
337
|
response_model = ResponseLoginModel.from_dict(resp_dict)
|
|
317
338
|
except Exception as exc:
|
|
318
|
-
logger.debug('Error parsing
|
|
339
|
+
logger.debug('Error parsing auth response: %s', exc)
|
|
319
340
|
raise VeSyncAPIResponseError(
|
|
320
|
-
'Error receiving response to
|
|
341
|
+
'Error receiving response to auth request'
|
|
321
342
|
) from exc
|
|
322
343
|
result = response_model.result
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
logger.debug('Login successful')
|
|
328
|
-
return True
|
|
344
|
+
if isinstance(result, IntRespAuthResultModel):
|
|
345
|
+
# If the result is an IntRespAuthResultModel,
|
|
346
|
+
# we need to log in with the auth code
|
|
347
|
+
return await self._login_token(auth_code=result.authorizeCode)
|
|
329
348
|
|
|
330
349
|
error_info = ErrorCodes.get_error_info(resp_dict.get("code"))
|
|
331
350
|
resp_message = resp_dict.get('msg')
|
|
332
351
|
if resp_message is not None:
|
|
333
352
|
error_info.message = f'{error_info.message} ({resp_message})'
|
|
334
|
-
raise VeSyncAPIResponseError(
|
|
353
|
+
raise VeSyncAPIResponseError(
|
|
354
|
+
f"Error receiving response to auth request - {error_info.message}"
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
async def _login_token(
|
|
358
|
+
self,
|
|
359
|
+
auth_code: str | None = None,
|
|
360
|
+
region_change_token: str | None = None,
|
|
361
|
+
) -> bool: # pylint: disable=W9006 # pylint mult docstring raises
|
|
362
|
+
"""Exchanges the authorization code for a token.
|
|
363
|
+
|
|
364
|
+
This completes the login process. If the initial call fails with
|
|
365
|
+
`"login trigger cross region error."`, the region is adjusted and
|
|
366
|
+
another attempt is made.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
auth_code (str): Auth code to use for logging in.
|
|
370
|
+
region_change_token (str): "bizToken" to use when calling this endpoint
|
|
371
|
+
for a second time with a different region.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
True if login successful, False if not.
|
|
375
|
+
|
|
376
|
+
Raises:
|
|
377
|
+
VeSyncLoginError: If login fails due to invalid username or password.
|
|
378
|
+
VeSyncAPIResponseError: If API response is invalid.
|
|
379
|
+
VeSyncServerError: If server returns an error.
|
|
380
|
+
"""
|
|
381
|
+
request_login = RequestLoginModel(
|
|
382
|
+
method='loginByAuthorizeCode4Vesync',
|
|
383
|
+
authorizeCode=auth_code,
|
|
384
|
+
bizToken=region_change_token,
|
|
385
|
+
userCountryCode=self.country_code,
|
|
386
|
+
regionChange="last_region" if region_change_token else None,
|
|
387
|
+
)
|
|
388
|
+
resp_dict, _ = await self.async_call_api(
|
|
389
|
+
'/user/api/accountManage/v1/loginByAuthorizeCode4Vesync', 'post',
|
|
390
|
+
json_object=request_login
|
|
391
|
+
)
|
|
392
|
+
if resp_dict is None:
|
|
393
|
+
raise VeSyncAPIResponseError('Error receiving response to login request')
|
|
394
|
+
try:
|
|
395
|
+
response_model = ResponseLoginModel.from_dict(resp_dict)
|
|
396
|
+
if not isinstance(response_model.result, IntRespLoginResultModel):
|
|
397
|
+
raise VeSyncAPIResponseError(
|
|
398
|
+
"Error receiving response to login request -"
|
|
399
|
+
"result is not IntRespLoginResultModel"
|
|
400
|
+
)
|
|
401
|
+
if response_model.code == 0:
|
|
402
|
+
result = response_model.result
|
|
403
|
+
if not isinstance(result, IntRespLoginResultModel):
|
|
404
|
+
raise VeSyncAPIResponseError(
|
|
405
|
+
"Error receiving response to login request -"
|
|
406
|
+
" result is not IntRespLoginResultModel"
|
|
407
|
+
)
|
|
408
|
+
self._token = result.token
|
|
409
|
+
self._account_id = result.accountID
|
|
410
|
+
self.country_code = result.countryCode
|
|
411
|
+
self.enabled = True
|
|
412
|
+
logger.debug('Login successful')
|
|
413
|
+
return True
|
|
414
|
+
error_info = ErrorCodes.get_error_info(resp_dict.get("code"))
|
|
415
|
+
if error_info.error_type == ErrorTypes.CROSS_REGION: # "cross region error."
|
|
416
|
+
result = response_model.result
|
|
417
|
+
self.country_code = result.countryCode
|
|
418
|
+
return await self._login_token(region_change_token=result.bizToken)
|
|
419
|
+
resp_message = resp_dict.get('msg')
|
|
420
|
+
if resp_message is not None:
|
|
421
|
+
error_info.message = f'{error_info.message} ({resp_message})'
|
|
422
|
+
raise VesyncLoginError(
|
|
423
|
+
f"Error receiving response to login request - {error_info.message}"
|
|
424
|
+
)
|
|
425
|
+
except (MissingField, UnserializableDataError) as exc:
|
|
426
|
+
logger.debug('Error parsing login response: %s', exc)
|
|
427
|
+
raise VeSyncAPIResponseError(
|
|
428
|
+
'Error receiving response to login request'
|
|
429
|
+
) from exc
|
|
335
430
|
|
|
336
431
|
async def update(self) -> None:
|
|
337
432
|
"""Fetch updated information about devices and new device list.
|
|
@@ -378,11 +473,11 @@ class VeSync: # pylint: disable=function-redefined
|
|
|
378
473
|
) -> tuple[dict | None, int | None]:
|
|
379
474
|
"""Make API calls by passing endpoint, header and body.
|
|
380
475
|
|
|
381
|
-
api argument is appended to
|
|
476
|
+
api argument is appended to `API_BASE_URL`.
|
|
382
477
|
Raises VeSyncRateLimitError if API returns a rate limit error.
|
|
383
478
|
|
|
384
479
|
Args:
|
|
385
|
-
api (str): Endpoint to call with
|
|
480
|
+
api (str): Endpoint to call with `API_BASE_URL`.
|
|
386
481
|
method (str): HTTP method to use.
|
|
387
482
|
json_object (dict | RequestBaseModel): JSON object to send in body.
|
|
388
483
|
headers (dict): Headers to send with request.
|
|
@@ -416,7 +511,7 @@ class VeSync: # pylint: disable=function-redefined
|
|
|
416
511
|
try:
|
|
417
512
|
async with self.session.request(
|
|
418
513
|
method,
|
|
419
|
-
url=
|
|
514
|
+
url=self._api_base_url_for_current_region() + api,
|
|
420
515
|
json=req_dict,
|
|
421
516
|
headers=headers,
|
|
422
517
|
raise_for_status=False,
|
|
@@ -447,6 +542,19 @@ class VeSync: # pylint: disable=function-redefined
|
|
|
447
542
|
LibraryLogger.log_api_exception(logger, exception=e, request_body=req_dict)
|
|
448
543
|
raise
|
|
449
544
|
|
|
545
|
+
def _api_base_url_for_current_region(self) -> str:
|
|
546
|
+
"""Retrieve the API base url for the current region.
|
|
547
|
+
|
|
548
|
+
At this point, only two different URLs exist: One for `EU` region
|
|
549
|
+
(for all EU countries), and one for all others
|
|
550
|
+
(currently `US`, `CA`, `MX`, `JP` - also used as a fallback).
|
|
551
|
+
|
|
552
|
+
If `API_BASE_URL` is set, it will take precedence over the determined URL.
|
|
553
|
+
"""
|
|
554
|
+
if self.country_code in NON_EU_REGIONS:
|
|
555
|
+
return API_BASE_URL_US
|
|
556
|
+
return API_BASE_URL_EU
|
|
557
|
+
|
|
450
558
|
def _update_fw_version(self, info_list: list[FirmwareDeviceItemModel]) -> bool:
|
|
451
559
|
"""Update device firmware versions from API response."""
|
|
452
560
|
if not info_list:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyvesync
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.0b4
|
|
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>
|
|
@@ -525,3 +525,20 @@ After:
|
|
|
525
525
|
All [contributions](CONTRIBUTING.md) are welcome.
|
|
526
526
|
|
|
527
527
|
This project is licensed under [MIT](LICENSE).
|
|
528
|
+
|
|
529
|
+
## Contributors
|
|
530
|
+
|
|
531
|
+
This is an open source project and cannot exist without the contributions of its community. Thank you to all the contributors who have helped make this project better!
|
|
532
|
+
|
|
533
|
+
A special thanks for helping with V3 go live:
|
|
534
|
+
|
|
535
|
+
| [<img src="https://github.com/cdninja.png" width="60px;"/><br /><sub><a href="https://github.com/cdninja">cdninja</a></sub>](https://github.com/cdninja) | [<img src="https://github.com/sapuseven.png" width="60px;"/><br /><sub><a href="https://github.com/sapuseven">sapuseven</a></sub>](https://github.com/sapuseven) | [<img src="https://github.com/sdrapha.png" width="60px;"/><br /><sub><a href="https://github.com/sdrapha">sdrapha</a></sub>](https://github.com/sdrapha) |
|
|
536
|
+
| :------- | :------- | :------- |
|
|
537
|
+
|
|
538
|
+
And to all of those that contributed to the project:
|
|
539
|
+
|
|
540
|
+
<a href="https://github.com/webdjoe/pyvesync/graphs/contributors">
|
|
541
|
+
<img src="https://contrib.rocks/image?repo=webdjoe/pyvesync" />
|
|
542
|
+
</a>
|
|
543
|
+
|
|
544
|
+
Made with [contrib.rocks](https://contrib.rocks).
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|