pyvesync 3.0.0b0__tar.gz → 3.0.0b2__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.0b0/src/pyvesync.egg-info → pyvesync-3.0.0b2}/PKG-INFO +2 -2
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/README.md +1 -1
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/pyproject.toml +1 -1
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/base_devices/fan_base.py +13 -22
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/base_devices/humidifier_base.py +19 -22
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/base_devices/outlet_base.py +7 -7
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/base_devices/purifier_base.py +18 -23
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/base_devices/switch_base.py +5 -5
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/base_devices/vesyncbasedevice.py +6 -5
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/const.py +6 -6
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/device_container.py +1 -1
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/device_map.py +1 -1
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/devices/vesynchumidifier.py +5 -4
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/devices/vesyncpurifier.py +9 -5
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/models/fan_models.py +1 -1
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/models/home_models.py +8 -8
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/models/humidifier_models.py +2 -2
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/models/purifier_models.py +7 -8
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/models/vesync_models.py +63 -12
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/utils/errors.py +1 -1
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/utils/helpers.py +0 -2
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/utils/logs.py +6 -1
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/vesync.py +73 -4
- pyvesync-3.0.0b2/src/pyvesync/vesynchome.py +162 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2/src/pyvesync.egg-info}/PKG-INFO +2 -2
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync.egg-info/SOURCES.txt +1 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/LICENSE +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/MANIFEST.in +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/requirements.txt +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/setup.cfg +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/setup.py +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/__init__.py +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/base_devices/__init__.py +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/base_devices/bulb_base.py +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/base_devices/fryer_base.py +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/base_devices/thermostat_base.py +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/devices/__init__.py +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/devices/vesyncbulb.py +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/devices/vesyncfan.py +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/devices/vesynckitchen.py +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/devices/vesyncoutlet.py +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/devices/vesyncswitch.py +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/devices/vesyncthermostat.py +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/models/__init__.py +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/models/base_models.py +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/models/bulb_models.py +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/models/bypass_models.py +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/models/fryer_models.py +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/models/outlet_models.py +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/models/switch_models.py +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/models/thermostat_models.py +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/utils/__init__.py +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/utils/colors.py +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/utils/device_mixins.py +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/utils/enum_utils.py +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync.egg-info/dependency_links.txt +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync.egg-info/requires.txt +0 -0
- {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/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.0b2
|
|
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>
|
|
@@ -340,7 +340,7 @@ manager.devices.outlets = [VeSyncOutletInstances]
|
|
|
340
340
|
manager.devices.switches = [VeSyncSwitchInstances]
|
|
341
341
|
manager.devices.fans = [VeSyncFanInstances]
|
|
342
342
|
manager.devices.bulbs = [VeSyncBulbInstances]
|
|
343
|
-
manager.devices.
|
|
343
|
+
manager.devices.air_purifiers = [VeSyncPurifierInstances]
|
|
344
344
|
manager.devices.humidifiers = [VeSyncHumidifierInstances]
|
|
345
345
|
manager.devices.air_fryers = [VeSyncAirFryerInstances]
|
|
346
346
|
managers.devices.thermostats = [VeSyncThermostatInstances]
|
|
@@ -309,7 +309,7 @@ manager.devices.outlets = [VeSyncOutletInstances]
|
|
|
309
309
|
manager.devices.switches = [VeSyncSwitchInstances]
|
|
310
310
|
manager.devices.fans = [VeSyncFanInstances]
|
|
311
311
|
manager.devices.bulbs = [VeSyncBulbInstances]
|
|
312
|
-
manager.devices.
|
|
312
|
+
manager.devices.air_purifiers = [VeSyncPurifierInstances]
|
|
313
313
|
manager.devices.humidifiers = [VeSyncHumidifierInstances]
|
|
314
314
|
manager.devices.air_fryers = [VeSyncAirFryerInstances]
|
|
315
315
|
managers.devices.thermostats = [VeSyncThermostatInstances]
|
|
@@ -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.0b2"
|
|
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"
|
|
@@ -6,13 +6,7 @@ from abc import abstractmethod
|
|
|
6
6
|
from typing_extensions import deprecated
|
|
7
7
|
|
|
8
8
|
from pyvesync.base_devices.vesyncbasedevice import VeSyncBaseToggleDevice, DeviceState
|
|
9
|
-
from pyvesync.const import
|
|
10
|
-
FanModes,
|
|
11
|
-
FanFeatures,
|
|
12
|
-
DeviceStatus,
|
|
13
|
-
FanSleepPreference,
|
|
14
|
-
StrFlag,
|
|
15
|
-
)
|
|
9
|
+
from pyvesync.const import FanModes, FanFeatures
|
|
16
10
|
|
|
17
11
|
if TYPE_CHECKING:
|
|
18
12
|
from pyvesync import VeSync
|
|
@@ -26,10 +20,7 @@ logger = logging.getLogger(__name__)
|
|
|
26
20
|
class FanState(DeviceState):
|
|
27
21
|
"""Base state class for Purifiers.
|
|
28
22
|
|
|
29
|
-
Not all attributes are supported by all devices.
|
|
30
|
-
UNKNOWN by default and attributes that are not supported by all devices are
|
|
31
|
-
set to [StrFlag.NOT_SUPPORTED][pyvesync.const.StrFlag] or
|
|
32
|
-
[IntFlag.NOT_SUPPORTED][pyvesync.const.IntFlag].
|
|
23
|
+
Not all attributes are supported by all devices.
|
|
33
24
|
|
|
34
25
|
|
|
35
26
|
Attributes:
|
|
@@ -93,17 +84,17 @@ class FanState(DeviceState):
|
|
|
93
84
|
self.humidity: int | None = None
|
|
94
85
|
self.temperature: int | None = None
|
|
95
86
|
self.thermal_comfort: int | None = None
|
|
96
|
-
self.sleep_preference_type: str | None =
|
|
97
|
-
self.sleep_fallasleep_remain: str | None =
|
|
98
|
-
self.sleep_oscillation_switch: str | None =
|
|
99
|
-
self.sleep_change_fan_level: str | None =
|
|
100
|
-
self.mute_status: str =
|
|
101
|
-
self.mute_set_state: str =
|
|
102
|
-
self.oscillation_status: str =
|
|
103
|
-
self.oscillation_set_state: str =
|
|
104
|
-
self.display_status: str =
|
|
105
|
-
self.display_set_state: str =
|
|
106
|
-
self.displaying_type: str =
|
|
87
|
+
self.sleep_preference_type: str | None = None
|
|
88
|
+
self.sleep_fallasleep_remain: str | None = None
|
|
89
|
+
self.sleep_oscillation_switch: str | None = None
|
|
90
|
+
self.sleep_change_fan_level: str | None = None
|
|
91
|
+
self.mute_status: str | None = None
|
|
92
|
+
self.mute_set_state: str | None = None
|
|
93
|
+
self.oscillation_status: str | None = None
|
|
94
|
+
self.oscillation_set_state: str | None = None
|
|
95
|
+
self.display_status: str | None = None
|
|
96
|
+
self.display_set_state: str | None = None
|
|
97
|
+
self.displaying_type: str | None = None
|
|
107
98
|
|
|
108
99
|
|
|
109
100
|
class VeSyncFanBase(VeSyncBaseToggleDevice):
|
|
@@ -9,9 +9,7 @@ from pyvesync.base_devices.vesyncbasedevice import VeSyncBaseToggleDevice, Devic
|
|
|
9
9
|
from pyvesync.const import (
|
|
10
10
|
HumidifierFeatures,
|
|
11
11
|
HumidifierModes,
|
|
12
|
-
DeviceStatus
|
|
13
|
-
IntFlag,
|
|
14
|
-
StrFlag
|
|
12
|
+
DeviceStatus
|
|
15
13
|
)
|
|
16
14
|
|
|
17
15
|
if TYPE_CHECKING:
|
|
@@ -27,8 +25,7 @@ class HumidifierState(DeviceState):
|
|
|
27
25
|
"""State Class for VeSync Humidifiers.
|
|
28
26
|
|
|
29
27
|
This is the state class for all vesync humidifiers. If there are
|
|
30
|
-
new features or state attributes
|
|
31
|
-
this class and be set to the default `NOT_SUPPORTED` flag.
|
|
28
|
+
new features or state attributes.
|
|
32
29
|
|
|
33
30
|
Attributes:
|
|
34
31
|
auto_preference (int): Auto preference level.
|
|
@@ -97,29 +94,29 @@ class HumidifierState(DeviceState):
|
|
|
97
94
|
"""
|
|
98
95
|
super().__init__(device, details, feature_map)
|
|
99
96
|
self.auto_stop_target_reached: bool = False
|
|
100
|
-
self.auto_target_humidity: int =
|
|
97
|
+
self.auto_target_humidity: int | None = None
|
|
101
98
|
self.automatic_stop_config: bool = False
|
|
102
99
|
self.display_set_status: str = DeviceStatus.UNKNOWN
|
|
103
100
|
self.display_status: str = DeviceStatus.UNKNOWN
|
|
104
|
-
self.humidity: int =
|
|
101
|
+
self.humidity: int | None = None
|
|
105
102
|
self.humidity_high: bool = False
|
|
106
103
|
self.mist_level: int | None = None
|
|
107
104
|
self.mist_virtual_level: int | None = None
|
|
108
|
-
self.mode: str =
|
|
109
|
-
self.nightlight_brightness: int =
|
|
110
|
-
self.nightlight_status: str =
|
|
105
|
+
self.mode: str | None = None
|
|
106
|
+
self.nightlight_brightness: int | None = None
|
|
107
|
+
self.nightlight_status: str | None = None
|
|
111
108
|
self.warm_mist_enabled: bool | None = None
|
|
112
|
-
self.warm_mist_level: int =
|
|
109
|
+
self.warm_mist_level: int | None = None
|
|
113
110
|
self.water_lacks: bool = False
|
|
114
111
|
self.water_tank_lifted: bool = False
|
|
115
|
-
self.temperature: int =
|
|
112
|
+
self.temperature: int | None = None
|
|
116
113
|
# Superior 6000S States
|
|
117
|
-
self.auto_preference: int =
|
|
118
|
-
self.filter_life_percent: int =
|
|
119
|
-
self.drying_mode_level: int =
|
|
120
|
-
self.drying_mode_auto_switch: str =
|
|
121
|
-
self.drying_mode_status: str =
|
|
122
|
-
self.drying_mode_time_remain: int =
|
|
114
|
+
self.auto_preference: int | None = None
|
|
115
|
+
self.filter_life_percent: int | None = None
|
|
116
|
+
self.drying_mode_level: int | None = None
|
|
117
|
+
self.drying_mode_auto_switch: str | None = None
|
|
118
|
+
self.drying_mode_status: str | None = None
|
|
119
|
+
self.drying_mode_time_remain: int | None = None
|
|
123
120
|
|
|
124
121
|
@property
|
|
125
122
|
def automatic_stop(self) -> bool:
|
|
@@ -141,7 +138,7 @@ class HumidifierState(DeviceState):
|
|
|
141
138
|
return self.auto_stop_target_reached
|
|
142
139
|
|
|
143
140
|
@property
|
|
144
|
-
def target_humidity(self) -> int:
|
|
141
|
+
def target_humidity(self) -> int | None:
|
|
145
142
|
"""Return the target humidity level.
|
|
146
143
|
|
|
147
144
|
Returns:
|
|
@@ -150,7 +147,7 @@ class HumidifierState(DeviceState):
|
|
|
150
147
|
return self.auto_target_humidity
|
|
151
148
|
|
|
152
149
|
@property
|
|
153
|
-
def auto_humidity(self) -> int:
|
|
150
|
+
def auto_humidity(self) -> int | None:
|
|
154
151
|
"""Return the auto humidity level.
|
|
155
152
|
|
|
156
153
|
Returns:
|
|
@@ -174,7 +171,7 @@ class HumidifierState(DeviceState):
|
|
|
174
171
|
return self.humidity
|
|
175
172
|
|
|
176
173
|
@property
|
|
177
|
-
def drying_mode_state(self) -> str:
|
|
174
|
+
def drying_mode_state(self) -> str | None:
|
|
178
175
|
"""Return the drying mode state.
|
|
179
176
|
|
|
180
177
|
Returns:
|
|
@@ -183,7 +180,7 @@ class HumidifierState(DeviceState):
|
|
|
183
180
|
return self.drying_mode_status
|
|
184
181
|
|
|
185
182
|
@property
|
|
186
|
-
def drying_mode_seconds_remaining(self) -> int:
|
|
183
|
+
def drying_mode_seconds_remaining(self) -> int | None:
|
|
187
184
|
"""Return the drying mode seconds remaining.
|
|
188
185
|
|
|
189
186
|
Return:
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import logging
|
|
4
4
|
from typing import TYPE_CHECKING
|
|
5
5
|
|
|
6
|
-
from pyvesync.const import OutletFeatures,
|
|
6
|
+
from pyvesync.const import OutletFeatures, NightlightModes
|
|
7
7
|
from pyvesync.utils.helpers import Helpers
|
|
8
8
|
from pyvesync.base_devices.vesyncbasedevice import VeSyncBaseToggleDevice, DeviceState
|
|
9
9
|
from pyvesync.models.outlet_models import RequestEnergyHistory, ResponseEnergyHistory
|
|
@@ -89,12 +89,12 @@ class OutletState(DeviceState):
|
|
|
89
89
|
self.device: VeSyncOutlet = device
|
|
90
90
|
self.features: list[str] = feature_map.features
|
|
91
91
|
self.active_time: int | None = 0
|
|
92
|
-
self.power: float =
|
|
93
|
-
self.energy: float =
|
|
94
|
-
self.voltage: float =
|
|
95
|
-
self.nightlight_status: str | None =
|
|
96
|
-
self.nightlight_brightness: int | None =
|
|
97
|
-
self.nightlight_automode: str | None =
|
|
92
|
+
self.power: float | None = None
|
|
93
|
+
self.energy: float | None = None
|
|
94
|
+
self.voltage: float | None = None
|
|
95
|
+
self.nightlight_status: str | None = None
|
|
96
|
+
self.nightlight_brightness: int | None = None
|
|
97
|
+
self.nightlight_automode: str | None = None
|
|
98
98
|
self.weekly_history: ResponseEnergyResult | None = None
|
|
99
99
|
self.monthly_history: ResponseEnergyResult | None = None
|
|
100
100
|
self.yearly_history: ResponseEnergyResult | None = None
|
|
@@ -4,7 +4,7 @@ import logging
|
|
|
4
4
|
from typing import TYPE_CHECKING
|
|
5
5
|
from abc import abstractmethod
|
|
6
6
|
|
|
7
|
-
from
|
|
7
|
+
from typing_extensions import deprecated
|
|
8
8
|
|
|
9
9
|
from pyvesync.base_devices.vesyncbasedevice import VeSyncBaseToggleDevice, DeviceState
|
|
10
10
|
from pyvesync.const import (
|
|
@@ -12,10 +12,7 @@ from pyvesync.const import (
|
|
|
12
12
|
PurifierFeatures,
|
|
13
13
|
PurifierModes,
|
|
14
14
|
DeviceStatus,
|
|
15
|
-
NightlightModes
|
|
16
|
-
PurifierAutoPreference,
|
|
17
|
-
IntFlag,
|
|
18
|
-
StrFlag,
|
|
15
|
+
NightlightModes
|
|
19
16
|
)
|
|
20
17
|
|
|
21
18
|
if TYPE_CHECKING:
|
|
@@ -97,25 +94,23 @@ class PurifierState(DeviceState):
|
|
|
97
94
|
self.fan_level: int | None = None
|
|
98
95
|
self.fan_set_level: int | None = None
|
|
99
96
|
self.filter_life: int | None = None
|
|
100
|
-
self.auto_preference_type: str | None =
|
|
97
|
+
self.auto_preference_type: str | None = None
|
|
101
98
|
self.auto_room_size: int | None = None
|
|
102
|
-
self._air_quality_level: AirQualityLevel =
|
|
99
|
+
self._air_quality_level: AirQualityLevel | None = None
|
|
103
100
|
self.child_lock: bool = False
|
|
104
101
|
self.filter_open_state: bool = False
|
|
105
|
-
self.display_status: str =
|
|
106
|
-
self.display_set_state: str =
|
|
102
|
+
self.display_status: str | None = None
|
|
103
|
+
self.display_set_state: str | None = None
|
|
107
104
|
self.display_forever: bool = False
|
|
108
105
|
# Attributes not supported by all purifiers
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
self.
|
|
112
|
-
self.
|
|
113
|
-
self.
|
|
114
|
-
self.
|
|
115
|
-
self.
|
|
116
|
-
self.
|
|
117
|
-
self.nightlight_status: str = StrFlag.NOT_SUPPORTED
|
|
118
|
-
self.fan_rotate_angle: int | None = IntFlag.NOT_SUPPORTED
|
|
106
|
+
self.pm25: int | None = None
|
|
107
|
+
self.pm1: int | None = None
|
|
108
|
+
self.pm10: int | None = None
|
|
109
|
+
self.aq_percent: int | None = None
|
|
110
|
+
self.light_detection_switch: str | None = None
|
|
111
|
+
self.light_detection_status: str | None = None
|
|
112
|
+
self.nightlight_status: str | None = None
|
|
113
|
+
self.fan_rotate_angle: int | None = None
|
|
119
114
|
|
|
120
115
|
@property
|
|
121
116
|
def air_quality_level(self) -> int:
|
|
@@ -171,19 +166,19 @@ class PurifierState(DeviceState):
|
|
|
171
166
|
|
|
172
167
|
@property
|
|
173
168
|
@deprecated("Use PurifierState.nightlight_status instead.")
|
|
174
|
-
def night_light(self) -> str:
|
|
169
|
+
def night_light(self) -> str | None:
|
|
175
170
|
"""Return night light status."""
|
|
176
171
|
return self.nightlight_status
|
|
177
172
|
|
|
178
173
|
@property
|
|
179
174
|
@deprecated("Use display_status instead.")
|
|
180
|
-
def display_state(self) -> str:
|
|
175
|
+
def display_state(self) -> str | None:
|
|
181
176
|
"""Return display status."""
|
|
182
177
|
return self.display_status
|
|
183
178
|
|
|
184
179
|
@property
|
|
185
180
|
@deprecated("Use display_set_state instead.")
|
|
186
|
-
def display_switch(self) -> str:
|
|
181
|
+
def display_switch(self) -> str | None:
|
|
187
182
|
"""Return display switch status."""
|
|
188
183
|
return self.display_set_state
|
|
189
184
|
|
|
@@ -499,7 +494,7 @@ class VeSyncPurifier(VeSyncBaseToggleDevice):
|
|
|
499
494
|
|
|
500
495
|
@property
|
|
501
496
|
@deprecated("Use self.state.nightlight_status instead.")
|
|
502
|
-
def night_light(self) -> str:
|
|
497
|
+
def night_light(self) -> str | None:
|
|
503
498
|
"""Get night light state.
|
|
504
499
|
|
|
505
500
|
Returns:
|
|
@@ -7,7 +7,7 @@ from typing_extensions import deprecated
|
|
|
7
7
|
from pyvesync.base_devices.vesyncbasedevice import VeSyncBaseToggleDevice, DeviceState
|
|
8
8
|
|
|
9
9
|
from pyvesync.utils.colors import Color
|
|
10
|
-
from pyvesync.const import SwitchFeatures
|
|
10
|
+
from pyvesync.const import SwitchFeatures
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
13
|
from pyvesync import VeSync
|
|
@@ -57,11 +57,11 @@ class SwitchState(DeviceState):
|
|
|
57
57
|
"""Initialize VeSync Switch State."""
|
|
58
58
|
super().__init__(device, details, feature_map)
|
|
59
59
|
self.device: VeSyncSwitch = device
|
|
60
|
-
self._backlight_color: Color |
|
|
61
|
-
self.brightness: int =
|
|
60
|
+
self._backlight_color: Color | None = None
|
|
61
|
+
self.brightness: int | None = None
|
|
62
62
|
self.active_time: int | None = 0
|
|
63
|
-
self.backlight_status: str =
|
|
64
|
-
self.indicator_status: str =
|
|
63
|
+
self.backlight_status: str | None = None
|
|
64
|
+
self.indicator_status: str | None = None
|
|
65
65
|
|
|
66
66
|
@property
|
|
67
67
|
def backlight_rgb(self) -> RGB | None:
|
|
@@ -10,7 +10,7 @@ from typing_extensions import deprecated
|
|
|
10
10
|
# import warnings
|
|
11
11
|
import orjson
|
|
12
12
|
|
|
13
|
-
from pyvesync.const import DeviceStatus, ConnectionStatus
|
|
13
|
+
from pyvesync.const import DeviceStatus, ConnectionStatus
|
|
14
14
|
|
|
15
15
|
logger = logging.getLogger(__name__)
|
|
16
16
|
|
|
@@ -53,6 +53,7 @@ class VeSyncBaseDevice(ABC, Generic[VS_STATE_T]):
|
|
|
53
53
|
config_module (str): Configuration module of device.
|
|
54
54
|
mac_id (str): MAC ID of device.
|
|
55
55
|
current_firm_version (str): Current firmware version of device.
|
|
56
|
+
latest_firm_version (str | None): Latest firmware version of device.
|
|
56
57
|
device_region (str): Region of device. (US, EU, etc.)
|
|
57
58
|
pid (str): Product ID of device, pulled by some devices on update.
|
|
58
59
|
sub_device_no (int): Sub-device number of device.
|
|
@@ -96,6 +97,7 @@ class VeSyncBaseDevice(ABC, Generic[VS_STATE_T]):
|
|
|
96
97
|
"enabled",
|
|
97
98
|
"features",
|
|
98
99
|
"last_response",
|
|
100
|
+
"latest_firm_version",
|
|
99
101
|
"mac_id",
|
|
100
102
|
"manager",
|
|
101
103
|
"pid",
|
|
@@ -129,9 +131,10 @@ class VeSyncBaseDevice(ABC, Generic[VS_STATE_T]):
|
|
|
129
131
|
self.config_module: str = details.configModule
|
|
130
132
|
self.mac_id: str | None = details.macID
|
|
131
133
|
self.current_firm_version = details.currentFirmVersion
|
|
134
|
+
self.latest_firm_version: str | None = None
|
|
132
135
|
self.device_region: str | None = details.deviceRegion
|
|
133
136
|
self.pid: str | None = None
|
|
134
|
-
self.sub_device_no: int = details.subDeviceNo
|
|
137
|
+
self.sub_device_no: int | None = details.subDeviceNo
|
|
135
138
|
# From feature_map
|
|
136
139
|
self.product_type: str = feature_map.product_type
|
|
137
140
|
self.features: list[str] = feature_map.features
|
|
@@ -192,7 +195,7 @@ class VeSyncBaseDevice(ABC, Generic[VS_STATE_T]):
|
|
|
192
195
|
|
|
193
196
|
This is going to be updated.
|
|
194
197
|
"""
|
|
195
|
-
return
|
|
198
|
+
return self.latest_firm_version != self.current_firm_version
|
|
196
199
|
|
|
197
200
|
async def set_timer(self, duration: int, action: str | None = None) -> bool:
|
|
198
201
|
"""Set timer for device.
|
|
@@ -542,8 +545,6 @@ class DeviceState:
|
|
|
542
545
|
or attr_name in [*self.__base_exclusions, *self._exclude_serialization]
|
|
543
546
|
):
|
|
544
547
|
continue
|
|
545
|
-
if attr_value in [IntFlag.NOT_SUPPORTED, StrFlag.NOT_SUPPORTED]:
|
|
546
|
-
continue
|
|
547
548
|
state_dict[attr_name] = attr_value
|
|
548
549
|
return state_dict
|
|
549
550
|
|
|
@@ -21,7 +21,7 @@ Attributes:
|
|
|
21
21
|
TERMINAL_ID (str): Unique identifier for new API calls
|
|
22
22
|
"""
|
|
23
23
|
from random import randint
|
|
24
|
-
from uuid import
|
|
24
|
+
from uuid import uuid4
|
|
25
25
|
from enum import StrEnum, IntEnum, Enum
|
|
26
26
|
from types import MappingProxyType
|
|
27
27
|
from pyvesync.utils.enum_utils import IntEnumMixin
|
|
@@ -36,15 +36,15 @@ USER_AGENT = (
|
|
|
36
36
|
)
|
|
37
37
|
DEFAULT_TZ = "America/New_York"
|
|
38
38
|
DEFAULT_REGION = "US"
|
|
39
|
-
APP_VERSION = "
|
|
39
|
+
APP_VERSION = "5.5.60"
|
|
40
40
|
PHONE_BRAND = "SM N9005"
|
|
41
41
|
PHONE_OS = "Android"
|
|
42
42
|
# MOBILE_ID = "1234567890123456"
|
|
43
43
|
MOBILE_ID = str(randint(1000000000000000, 9999999999999999)) # noqa: S311
|
|
44
44
|
USER_TYPE = "1"
|
|
45
|
-
BYPASS_APP_V = "VeSync
|
|
45
|
+
BYPASS_APP_V = f"VeSync {APP_VERSION}"
|
|
46
46
|
BYPASS_HEADER_UA = "okhttp/3.12.1"
|
|
47
|
-
TERMINAL_ID = '2' +
|
|
47
|
+
TERMINAL_ID = '2' + str(uuid4()).replace('-', '')
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
# Generic Constants
|
|
@@ -64,7 +64,7 @@ class ProductTypes(StrEnum):
|
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
class IntFlag(IntEnum):
|
|
67
|
-
"""Integer flag to indicate if a device is not supported.
|
|
67
|
+
"""DEPRECATED. Integer flag to indicate if a device is not supported.
|
|
68
68
|
|
|
69
69
|
This is used by data models as a default value for feature attributes
|
|
70
70
|
that are not supported by all devices.
|
|
@@ -82,7 +82,7 @@ class IntFlag(IntEnum):
|
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
class StrFlag(StrEnum):
|
|
85
|
-
"""String flag to indicate if a device is not supported.
|
|
85
|
+
"""DEPRECATED. String flag to indicate if a device is not supported.
|
|
86
86
|
|
|
87
87
|
This is used by data models as a default value for feature attributes
|
|
88
88
|
that are not supported by all devices.
|
|
@@ -184,7 +184,7 @@ class DeviceContainer(_DeviceContainerBase):
|
|
|
184
184
|
if device_obj is not None:
|
|
185
185
|
self.add(device_obj)
|
|
186
186
|
|
|
187
|
-
def device_exists(self, cid: str, sub_device_no: int =
|
|
187
|
+
def device_exists(self, cid: str, sub_device_no: int | None = None) -> bool:
|
|
188
188
|
"""Check if a device with the given cid & sub_dev_no exists.
|
|
189
189
|
|
|
190
190
|
Args:
|
|
@@ -746,7 +746,7 @@ purifier_modules: list[PurifierMap] = [
|
|
|
746
746
|
dev_types=["LV-PUR131S", "LV-RH131S"],
|
|
747
747
|
modes=[PurifierModes.SLEEP, PurifierModes.MANUAL, PurifierModes.AUTO],
|
|
748
748
|
features=[PurifierFeatures.AIR_QUALITY],
|
|
749
|
-
fan_levels=list(range(1,
|
|
749
|
+
fan_levels=list(range(1, 4)),
|
|
750
750
|
device_alias="LV-PUR131S",
|
|
751
751
|
model_display="LV-PUR131S/RH131S Series",
|
|
752
752
|
model_name="LV131S",
|
|
@@ -8,7 +8,7 @@ import orjson
|
|
|
8
8
|
from pyvesync.base_devices import VeSyncHumidifier
|
|
9
9
|
from pyvesync.utils.helpers import Helpers, Validators, Timer
|
|
10
10
|
from pyvesync.utils.device_mixins import BypassV2Mixin, process_bypassv2_result
|
|
11
|
-
from pyvesync.const import DeviceStatus,
|
|
11
|
+
from pyvesync.const import DeviceStatus, ConnectionStatus
|
|
12
12
|
from pyvesync.models.bypass_models import ResultV2GetTimer, ResultV2SetTimer
|
|
13
13
|
from pyvesync.models.humidifier_models import (
|
|
14
14
|
ClassicLVHumidResult,
|
|
@@ -75,7 +75,7 @@ class VeSyncHumid200300S(BypassV2Mixin, VeSyncHumidifier):
|
|
|
75
75
|
self.state.connection_status = ConnectionStatus.ONLINE
|
|
76
76
|
self.state.device_status = DeviceStatus.from_bool(resp_model.enabled)
|
|
77
77
|
self.state.mode = resp_model.mode
|
|
78
|
-
self.state.humidity = resp_model.humidity
|
|
78
|
+
self.state.humidity = resp_model.humidity
|
|
79
79
|
self.state.mist_virtual_level = resp_model.mist_virtual_level or 0
|
|
80
80
|
self.state.mist_level = resp_model.mist_level or 0
|
|
81
81
|
self.state.water_lacks = resp_model.water_lacks
|
|
@@ -84,7 +84,7 @@ class VeSyncHumid200300S(BypassV2Mixin, VeSyncHumidifier):
|
|
|
84
84
|
self.state.auto_stop_target_reached = resp_model.automatic_stop_reach_target
|
|
85
85
|
if (
|
|
86
86
|
self.supports_nightlight
|
|
87
|
-
and resp_model.night_light_brightness
|
|
87
|
+
and resp_model.night_light_brightness is not None
|
|
88
88
|
):
|
|
89
89
|
self.state.nightlight_brightness = resp_model.night_light_brightness
|
|
90
90
|
self.state.nightlight_status = (
|
|
@@ -93,7 +93,7 @@ class VeSyncHumid200300S(BypassV2Mixin, VeSyncHumidifier):
|
|
|
93
93
|
else DeviceStatus.OFF
|
|
94
94
|
)
|
|
95
95
|
self.state.display_status = DeviceStatus.from_bool(resp_model.display)
|
|
96
|
-
if self.supports_warm_mist and resp_model.warm_level
|
|
96
|
+
if self.supports_warm_mist and resp_model.warm_level is not None:
|
|
97
97
|
self.state.warm_mist_level = resp_model.warm_level
|
|
98
98
|
self.state.warm_mist_enabled = resp_model.warm_enabled
|
|
99
99
|
config = resp_model.configuration
|
|
@@ -388,6 +388,7 @@ class VeSyncHumid200S(VeSyncHumid200300S):
|
|
|
388
388
|
return False
|
|
389
389
|
|
|
390
390
|
self.state.display_set_status = DeviceStatus.from_bool(toggle)
|
|
391
|
+
self.state.display_status = DeviceStatus.from_bool(toggle)
|
|
391
392
|
self.state.connection_status = ConnectionStatus.ONLINE
|
|
392
393
|
return True
|
|
393
394
|
|
|
@@ -14,8 +14,6 @@ from pyvesync.utils.device_mixins import (
|
|
|
14
14
|
from pyvesync.utils.helpers import Helpers, Timer
|
|
15
15
|
# from pyvesync.models.base_models import DefaultValues
|
|
16
16
|
from pyvesync.const import (
|
|
17
|
-
IntFlag,
|
|
18
|
-
StrFlag,
|
|
19
17
|
PurifierAutoPreference,
|
|
20
18
|
DeviceStatus,
|
|
21
19
|
ConnectionStatus,
|
|
@@ -135,7 +133,7 @@ class VeSyncAirBypass(BypassV2Mixin, VeSyncPurifier):
|
|
|
135
133
|
if self.supports_air_quality is True:
|
|
136
134
|
self.state.pm25 = result.air_quality_value
|
|
137
135
|
self.state.set_air_quality_level(result.air_quality)
|
|
138
|
-
if result.night_light
|
|
136
|
+
if result.night_light is not None:
|
|
139
137
|
self.state.nightlight_status = DeviceStatus.ON if result.night_light \
|
|
140
138
|
else DeviceStatus.OFF
|
|
141
139
|
|
|
@@ -490,7 +488,7 @@ class VeSyncAirBaseV2(VeSyncAirBypass):
|
|
|
490
488
|
self.state.pm10 = details.PM10
|
|
491
489
|
self.state.aq_percent = details.AQPercent
|
|
492
490
|
self.state.fan_rotate_angle = details.fanRotateAngle
|
|
493
|
-
if details.filterOpenState
|
|
491
|
+
if details.filterOpenState is not None:
|
|
494
492
|
self.state.filter_open_state = bool(details.filterOpenState)
|
|
495
493
|
if details.timerRemain > 0:
|
|
496
494
|
self.state.timer = Timer(details.timerRemain, 'off')
|
|
@@ -812,7 +810,7 @@ class VeSyncAir131(BypassV1Mixin, VeSyncPurifier):
|
|
|
812
810
|
self.state.child_lock = bool(DeviceStatus(details.childLock))
|
|
813
811
|
self.state.mode = details.mode
|
|
814
812
|
self.state.fan_level = details.level or 0
|
|
815
|
-
self.state.fan_set_level = details.
|
|
813
|
+
self.state.fan_set_level = details.level or 0
|
|
816
814
|
self.state.set_air_quality_level(details.airQuality)
|
|
817
815
|
|
|
818
816
|
async def get_details(self) -> None:
|
|
@@ -928,6 +926,12 @@ class VeSyncAir131(BypassV1Mixin, VeSyncPurifier):
|
|
|
928
926
|
_LOGGER.debug('Invalid purifier mode used - %s', mode)
|
|
929
927
|
return False
|
|
930
928
|
|
|
929
|
+
if mode == PurifierModes.MANUAL:
|
|
930
|
+
set_level = (
|
|
931
|
+
1 if self.state.fan_set_level in [0, None] else self.state.fan_set_level
|
|
932
|
+
)
|
|
933
|
+
return await self.set_fan_speed(set_level)
|
|
934
|
+
|
|
931
935
|
update_dict = {
|
|
932
936
|
"mode": mode
|
|
933
937
|
}
|
|
@@ -20,7 +20,6 @@ from dataclasses import dataclass, field
|
|
|
20
20
|
|
|
21
21
|
from pyvesync.models.base_models import (
|
|
22
22
|
ResponseCodeModel,
|
|
23
|
-
ResponseBaseModel,
|
|
24
23
|
RequestBaseModel,
|
|
25
24
|
DefaultValues,
|
|
26
25
|
)
|
|
@@ -36,9 +35,9 @@ class RequestHomeModel(RequestBaseModel):
|
|
|
36
35
|
"""
|
|
37
36
|
# Arguments set by Manager Instance by passing kw_argument manager
|
|
38
37
|
# these fields should be set to init=False
|
|
39
|
-
accountID: str
|
|
40
|
-
token: str
|
|
41
|
-
userCountryCode: str
|
|
38
|
+
accountID: str
|
|
39
|
+
token: str
|
|
40
|
+
userCountryCode: str
|
|
42
41
|
# Non-default constants
|
|
43
42
|
method: str = 'getHomeList'
|
|
44
43
|
# default values
|
|
@@ -52,17 +51,17 @@ class RequestHomeModel(RequestBaseModel):
|
|
|
52
51
|
|
|
53
52
|
|
|
54
53
|
@dataclass
|
|
55
|
-
class ResponseHomeModel(
|
|
54
|
+
class ResponseHomeModel(ResponseCodeModel):
|
|
56
55
|
"""Model for the home data response.
|
|
57
56
|
|
|
58
|
-
Inherits from `
|
|
57
|
+
Inherits from `ResponseCodeModel`. The `ResponseCodeModel` class provides the
|
|
59
58
|
`code` and `msg` fields. The `ResponseHomeModel` class provides the `result`
|
|
60
59
|
field containing the home data.
|
|
61
60
|
|
|
62
61
|
Attributes:
|
|
63
62
|
result: dict: The home data.
|
|
64
63
|
"""
|
|
65
|
-
result: IntResponseHomeResultModel
|
|
64
|
+
result: IntResponseHomeResultModel | IntResponseHomeInfoResultModel | None
|
|
66
65
|
|
|
67
66
|
|
|
68
67
|
@dataclass
|
|
@@ -154,5 +153,6 @@ class IntResponseHomeResultModel:
|
|
|
154
153
|
@dataclass
|
|
155
154
|
class IntResponseHomeListModel:
|
|
156
155
|
"""Internal model for the 'homeList' field in home response result."""
|
|
157
|
-
|
|
156
|
+
homeId: int
|
|
158
157
|
homeName: str
|
|
158
|
+
nickname: str | None = None
|
|
@@ -73,8 +73,8 @@ class ClassicLVHumidResult(InnerHumidifierBaseResult):
|
|
|
73
73
|
automatic_stop_reach_target: bool = False
|
|
74
74
|
water_tank_lifted: bool = False
|
|
75
75
|
warm_enabled: bool = False
|
|
76
|
-
warm_level: int =
|
|
77
|
-
night_light_brightness: int =
|
|
76
|
+
warm_level: int | None = None
|
|
77
|
+
night_light_brightness: int | None = None
|
|
78
78
|
configuration: ClassicConfig | None = None
|
|
79
79
|
|
|
80
80
|
|
|
@@ -12,7 +12,6 @@ from dataclasses import dataclass
|
|
|
12
12
|
# import orjson
|
|
13
13
|
from mashumaro.config import BaseConfig
|
|
14
14
|
from mashumaro.types import Discriminator
|
|
15
|
-
from pyvesync.const import IntFlag, StrFlag
|
|
16
15
|
from pyvesync.models.bypass_models import (
|
|
17
16
|
RequestBypassV1,
|
|
18
17
|
BypassV2InnerResult,
|
|
@@ -53,11 +52,11 @@ class PurifierV2DetailsResult(InnerPurifierBaseResult):
|
|
|
53
52
|
efficientModeTimeRemain: int
|
|
54
53
|
errorCode: int
|
|
55
54
|
autoPreference: VitalAutoPreferences | None = None
|
|
56
|
-
fanRotateAngle: int | None =
|
|
57
|
-
filterOpenState: int | None =
|
|
58
|
-
PM1: int | None =
|
|
59
|
-
PM10: int | None =
|
|
60
|
-
AQPercent: int | None =
|
|
55
|
+
fanRotateAngle: int | None = None
|
|
56
|
+
filterOpenState: int | None = None
|
|
57
|
+
PM1: int | None = None
|
|
58
|
+
PM10: int | None = None
|
|
59
|
+
AQPercent: int | None = None
|
|
61
60
|
|
|
62
61
|
|
|
63
62
|
@dataclass
|
|
@@ -81,8 +80,8 @@ class PurifierCoreDetailsResult(InnerPurifierBaseResult):
|
|
|
81
80
|
configuration: PurifierCoreDetailsConfig | None = None
|
|
82
81
|
extension: dict | None = None
|
|
83
82
|
air_quality_value: int | None = None
|
|
84
|
-
night_light: str | None =
|
|
85
|
-
fan_rotate: str | None =
|
|
83
|
+
night_light: str | None = None
|
|
84
|
+
fan_rotate: str | None = None
|
|
86
85
|
|
|
87
86
|
|
|
88
87
|
# Purifier Timer Models
|
|
@@ -31,14 +31,19 @@ class RequestLoginModel(RequestBaseModel):
|
|
|
31
31
|
password: str
|
|
32
32
|
# default values
|
|
33
33
|
acceptLanguage: str = DefaultValues.acceptLanguage
|
|
34
|
-
|
|
34
|
+
accountID: str = ''
|
|
35
|
+
clientInfo: str = DefaultValues.phoneBrand
|
|
36
|
+
clientType: str = "vesyncApp"
|
|
37
|
+
clientVersion: str = f"VeSync {DefaultValues.appVersion}"
|
|
38
|
+
debugMode: bool = False
|
|
39
|
+
osInfo: str = DefaultValues.phoneOS
|
|
40
|
+
terminalId: str = DefaultValues.terminalId
|
|
35
41
|
timeZone: str = DefaultValues.timeZone
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
traceId: str = field(default_factory=DefaultValues.traceId)
|
|
39
|
-
# Non-default constants
|
|
40
|
-
userType: str = '1'
|
|
42
|
+
token: str = ''
|
|
43
|
+
userCountryCode: str = ''
|
|
41
44
|
devToken: str = ''
|
|
45
|
+
userType: str = DefaultValues.userType
|
|
46
|
+
traceId: str = field(default_factory=DefaultValues.newTraceId)
|
|
42
47
|
|
|
43
48
|
def __post_init__(self) -> None:
|
|
44
49
|
"""Set the method field."""
|
|
@@ -54,7 +59,7 @@ class RequestLoginModel(RequestBaseModel):
|
|
|
54
59
|
class IntRespLoginResultModel(ResponseBaseModel):
|
|
55
60
|
"""Model for the 'result' field in login response containing token and account ID.
|
|
56
61
|
|
|
57
|
-
This class is
|
|
62
|
+
This class is referenced by the `ResponseLoginModel` class.
|
|
58
63
|
"""
|
|
59
64
|
accountID: str
|
|
60
65
|
acceptLanguage: str
|
|
@@ -85,6 +90,7 @@ class ResponseLoginModel(ResponseCodeModel):
|
|
|
85
90
|
"accountID": "123456",
|
|
86
91
|
"acceptLanguage": "en",
|
|
87
92
|
"countryCode": "US",
|
|
93
|
+
"token": "abcdef1234567890"
|
|
88
94
|
}
|
|
89
95
|
}
|
|
90
96
|
b = ResponseLoginModel.from_dict(a)
|
|
@@ -93,8 +99,6 @@ class ResponseLoginModel(ResponseCodeModel):
|
|
|
93
99
|
```
|
|
94
100
|
"""
|
|
95
101
|
result: IntRespLoginResultModel
|
|
96
|
-
stacktrace: str | None
|
|
97
|
-
module: str | None
|
|
98
102
|
|
|
99
103
|
|
|
100
104
|
@dataclass
|
|
@@ -154,7 +158,7 @@ class ResponseDeviceDetailsModel(ResponseBaseModel):
|
|
|
154
158
|
subDeviceType: str | None
|
|
155
159
|
subDeviceList: None | str
|
|
156
160
|
extension: None | InternalDeviceListExtension
|
|
157
|
-
subDeviceNo: int =
|
|
161
|
+
subDeviceNo: int | None = None
|
|
158
162
|
deviceStatus: str = "off"
|
|
159
163
|
connectionStatus: str = "offline"
|
|
160
164
|
productType: str | None = None
|
|
@@ -182,8 +186,6 @@ class ResponseDeviceDetailsModel(ResponseBaseModel):
|
|
|
182
186
|
"""
|
|
183
187
|
super().__pre_deserialize__(d)
|
|
184
188
|
d = _flatten_device_prop(d)
|
|
185
|
-
if d.get('subDeviceNo') is None:
|
|
186
|
-
d['subDeviceNo'] = 0
|
|
187
189
|
if d.get('cid') is None:
|
|
188
190
|
d['cid'] = d.get('uuid') if d.get('uuid') is not None else d.get("macID")
|
|
189
191
|
return d
|
|
@@ -250,3 +252,52 @@ class RequestPID(RequestBaseModel):
|
|
|
250
252
|
mobileID: str
|
|
251
253
|
configModule: str
|
|
252
254
|
region: str
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@dataclass
|
|
258
|
+
class RequestFirmwareModel(RequestBaseModel):
|
|
259
|
+
"""Model for the firmware request."""
|
|
260
|
+
accountID: str
|
|
261
|
+
timeZone: str
|
|
262
|
+
token: str
|
|
263
|
+
userCountryCode: str
|
|
264
|
+
cidList: list[str]
|
|
265
|
+
acceptLanguage: str = DefaultValues.acceptLanguage
|
|
266
|
+
traceId: str = field(default_factory=DefaultValues.traceId)
|
|
267
|
+
appVersion: str = DefaultValues.appVersion
|
|
268
|
+
phoneBrand: str = DefaultValues.phoneBrand
|
|
269
|
+
phoneOS: str = DefaultValues.phoneOS
|
|
270
|
+
method: str = "getFirmwareUpdateInfoList"
|
|
271
|
+
debugMode: bool = False
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@dataclass
|
|
275
|
+
class ResponseFirmwareModel(ResponseCodeModel):
|
|
276
|
+
"""Model for the firmware response."""
|
|
277
|
+
result: FirmwareResultModel
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@dataclass
|
|
281
|
+
class FirmwareUpdateInfoModel(ResponseBaseModel):
|
|
282
|
+
"""Firmware update information model."""
|
|
283
|
+
currentVersion: str
|
|
284
|
+
latestVersion: str
|
|
285
|
+
releaseNotes: str
|
|
286
|
+
pluginName: str
|
|
287
|
+
isMainFw: bool
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@dataclass
|
|
291
|
+
class FirmwareDeviceItemModel(ResponseBaseModel):
|
|
292
|
+
"""Model for the firmware device item in the firmware response."""
|
|
293
|
+
deviceCid: str
|
|
294
|
+
deviceName: str
|
|
295
|
+
code: int
|
|
296
|
+
msg: str | None
|
|
297
|
+
firmUpdateInfos: list[FirmwareUpdateInfoModel]
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@dataclass
|
|
301
|
+
class FirmwareResultModel(ResponseBaseModel):
|
|
302
|
+
"""Model for the firmware response result."""
|
|
303
|
+
cidFwInfoList: list[FirmwareDeviceItemModel]
|
|
@@ -794,7 +794,7 @@ class VeSyncTokenError(VeSyncError):
|
|
|
794
794
|
|
|
795
795
|
def __init__(self) -> None:
|
|
796
796
|
"""Initialize the exception with a message."""
|
|
797
|
-
super().__init__("Token expired or invalid - please re-authenticate")
|
|
797
|
+
super().__init__("Token expired or invalid - please re-authenticate with login()")
|
|
798
798
|
|
|
799
799
|
|
|
800
800
|
class VeSyncServerError(VeSyncError):
|
|
@@ -19,6 +19,7 @@ from __future__ import annotations
|
|
|
19
19
|
import logging
|
|
20
20
|
import os
|
|
21
21
|
import re
|
|
22
|
+
from pathlib import Path
|
|
22
23
|
from collections.abc import Mapping
|
|
23
24
|
from dataclasses import fields, is_dataclass
|
|
24
25
|
from typing import TYPE_CHECKING
|
|
@@ -210,7 +211,11 @@ class LibraryLogger:
|
|
|
210
211
|
handler.setFormatter(formatter)
|
|
211
212
|
root_logger.addHandler(handler)
|
|
212
213
|
if file_name:
|
|
213
|
-
|
|
214
|
+
if not Path(file_name).is_absolute():
|
|
215
|
+
file_name_path = Path(file_name).joinpath(Path.cwd(), file_name)
|
|
216
|
+
else:
|
|
217
|
+
file_name_path = Path(file_name)
|
|
218
|
+
file_handler = logging.FileHandler(file_name_path)
|
|
214
219
|
file_handler.setFormatter(formatter)
|
|
215
220
|
root_logger.addHandler(file_handler)
|
|
216
221
|
for log_name, logger in root_logger.manager.loggerDict.items():
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
import logging
|
|
5
5
|
import asyncio
|
|
6
6
|
from typing import Self
|
|
7
|
+
from dataclasses import fields, MISSING
|
|
7
8
|
from aiohttp import ClientSession
|
|
8
9
|
from aiohttp.client_exceptions import ClientResponseError
|
|
9
10
|
from mashumaro.mixins.orjson import DataClassORJSONMixin
|
|
@@ -16,7 +17,10 @@ from pyvesync.models.vesync_models import (
|
|
|
16
17
|
RequestDeviceListModel,
|
|
17
18
|
ResponseDeviceListModel,
|
|
18
19
|
RequestLoginModel,
|
|
19
|
-
ResponseLoginModel
|
|
20
|
+
ResponseLoginModel,
|
|
21
|
+
RequestFirmwareModel,
|
|
22
|
+
ResponseFirmwareModel,
|
|
23
|
+
FirmwareDeviceItemModel
|
|
20
24
|
)
|
|
21
25
|
from pyvesync.utils.errors import (
|
|
22
26
|
ErrorCodes,
|
|
@@ -130,7 +134,6 @@ class VeSync: # pylint: disable=function-redefined
|
|
|
130
134
|
self._verbose: bool = False
|
|
131
135
|
self.time_zone: str = time_zone
|
|
132
136
|
self.language: str = 'en'
|
|
133
|
-
|
|
134
137
|
self.enabled = False
|
|
135
138
|
self.in_process = False
|
|
136
139
|
self._device_container: DeviceContainer = DeviceContainerInstance
|
|
@@ -299,11 +302,11 @@ class VeSync: # pylint: disable=function-redefined
|
|
|
299
302
|
raise VesyncLoginError('Username and password must be specified')
|
|
300
303
|
request_login = RequestLoginModel(
|
|
301
304
|
email=self.username,
|
|
302
|
-
method='
|
|
305
|
+
method='appLoginV3',
|
|
303
306
|
password=self.password,
|
|
304
307
|
)
|
|
305
308
|
resp_dict, _ = await self.async_call_api(
|
|
306
|
-
'/
|
|
309
|
+
'/user/api/accountManage/v3/appLoginV3', 'post',
|
|
307
310
|
json_object=request_login
|
|
308
311
|
)
|
|
309
312
|
if resp_dict is None:
|
|
@@ -443,3 +446,69 @@ class VeSync: # pylint: disable=function-redefined
|
|
|
443
446
|
except ClientResponseError as e:
|
|
444
447
|
LibraryLogger.log_api_exception(logger, exception=e, request_body=req_dict)
|
|
445
448
|
raise
|
|
449
|
+
|
|
450
|
+
def _update_fw_version(self, info_list: list[FirmwareDeviceItemModel]) -> bool:
|
|
451
|
+
"""Update device firmware versions from API response."""
|
|
452
|
+
if not info_list:
|
|
453
|
+
logger.debug('No devices found in firmware response')
|
|
454
|
+
return False
|
|
455
|
+
update_dict = {}
|
|
456
|
+
for device in info_list:
|
|
457
|
+
if not device.firmUpdateInfos:
|
|
458
|
+
if device.code != 0:
|
|
459
|
+
logger.debug(
|
|
460
|
+
'Device %s has error code %s with message: %s',
|
|
461
|
+
device.deviceName, device.code, device.msg
|
|
462
|
+
)
|
|
463
|
+
else:
|
|
464
|
+
logger.debug(
|
|
465
|
+
'Device %s has no firmware updates available',
|
|
466
|
+
device.deviceName
|
|
467
|
+
)
|
|
468
|
+
continue
|
|
469
|
+
for update_info in device.firmUpdateInfos:
|
|
470
|
+
update_dict[device.deviceCid] = (
|
|
471
|
+
update_info.currentVersion, update_info.latestVersion
|
|
472
|
+
)
|
|
473
|
+
if update_info.isMainFw is True:
|
|
474
|
+
break
|
|
475
|
+
for device_obj in self._device_container:
|
|
476
|
+
if device_obj.cid in update_dict:
|
|
477
|
+
device_obj.latest_firm_version = update_dict[device_obj.cid][1]
|
|
478
|
+
device_obj.current_firm_version = update_dict[device_obj.cid][0]
|
|
479
|
+
return True
|
|
480
|
+
|
|
481
|
+
async def check_firmware(self) -> bool:
|
|
482
|
+
"""Check for firmware updates for all devices.
|
|
483
|
+
|
|
484
|
+
This method will check for firmware updates for all devices in the
|
|
485
|
+
device container. It will call the `get_firmware_update()` method on
|
|
486
|
+
each device and log the results.
|
|
487
|
+
"""
|
|
488
|
+
if len(self._device_container) == 0:
|
|
489
|
+
logger.debug('No devices to check for firmware updates')
|
|
490
|
+
return False
|
|
491
|
+
body_fields = [
|
|
492
|
+
field.name for field in fields(RequestFirmwareModel)
|
|
493
|
+
if field.default_factory is MISSING and field.default is MISSING
|
|
494
|
+
]
|
|
495
|
+
body = Helpers.get_class_attributes(self, body_fields)
|
|
496
|
+
body['cidList'] = [device.cid for device in self._device_container]
|
|
497
|
+
resp_dict, _ = await self.async_call_api(
|
|
498
|
+
'/cloud/v2/deviceManaged/getFirmwareUpdateInfoList',
|
|
499
|
+
'post',
|
|
500
|
+
json_object=RequestFirmwareModel(**body),
|
|
501
|
+
)
|
|
502
|
+
if resp_dict is None:
|
|
503
|
+
raise VeSyncAPIResponseError(
|
|
504
|
+
'Error receiving response to firmware update request')
|
|
505
|
+
resp_model = ResponseFirmwareModel.from_dict(resp_dict)
|
|
506
|
+
if resp_model.code != 0:
|
|
507
|
+
error_info = ErrorCodes.get_error_info(resp_model.code)
|
|
508
|
+
resp_message = resp_model.msg
|
|
509
|
+
if resp_message is not None:
|
|
510
|
+
error_info.message = f'{error_info.message} ({resp_message})'
|
|
511
|
+
logger.debug('Error in firmware update response: %s', error_info.message)
|
|
512
|
+
return False
|
|
513
|
+
info_list = resp_model.result.cidFwInfoList
|
|
514
|
+
return self._update_fw_version(info_list)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""This is a WIP, not implemented yet."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import logging
|
|
4
|
+
import asyncio
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
from dataclasses import fields
|
|
7
|
+
|
|
8
|
+
from pyvesync.utils.errors import ErrorCodes, VeSyncAPIResponseError
|
|
9
|
+
from pyvesync.utils.helpers import Helpers
|
|
10
|
+
from pyvesync.models.home_models import (
|
|
11
|
+
IntResponseHomeResultModel,
|
|
12
|
+
IntResponseHomeListModel,
|
|
13
|
+
ResponseHomeModel,
|
|
14
|
+
RequestHomeModel,
|
|
15
|
+
)
|
|
16
|
+
from pyvesync.models.base_models import DefaultValues
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from pyvesync.vesync import VeSync
|
|
20
|
+
from pyvesync.models.base_models import RequestBaseModel
|
|
21
|
+
from pyvesync.base_devices import VeSyncBaseDevice
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_LOGGER = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class VeSyncHome:
|
|
28
|
+
"""Class to handle home-related operations in VeSync."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, home_id: int, name: str, nickname: str | None = None) -> None:
|
|
31
|
+
"""Initialize the VeSyncHome instance."""
|
|
32
|
+
self.home_id: int = home_id
|
|
33
|
+
self.name: str = name
|
|
34
|
+
self.nickname: str | None = nickname
|
|
35
|
+
self.rooms: list[VeSyncRoom] = []
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def devices(self) -> list[VeSyncBaseDevice]:
|
|
39
|
+
"""Return a list of all devices in the home."""
|
|
40
|
+
devices = []
|
|
41
|
+
for room in self.rooms:
|
|
42
|
+
devices.extend(room.devices)
|
|
43
|
+
return devices
|
|
44
|
+
|
|
45
|
+
async def update_devices(self, devices: list[VeSyncBaseDevice]) -> None:
|
|
46
|
+
"""Update the devices in the home."""
|
|
47
|
+
update_tasks = [device.update() for device in devices]
|
|
48
|
+
for update_coro in asyncio.as_completed(update_tasks):
|
|
49
|
+
try:
|
|
50
|
+
await update_coro
|
|
51
|
+
except Exception as e:
|
|
52
|
+
_LOGGER.debug('Error updating device %s', e)
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def _build_request_model(
|
|
56
|
+
manager: VeSync, request_model: type[RequestBaseModel]
|
|
57
|
+
) -> RequestBaseModel:
|
|
58
|
+
"""Build the request model for home data."""
|
|
59
|
+
req_fields = [field.name for field in fields(request_model) if field.init]
|
|
60
|
+
body = Helpers.get_class_attributes(DefaultValues, req_fields)
|
|
61
|
+
body.update(Helpers.get_class_attributes(manager, req_fields))
|
|
62
|
+
return request_model(**body)
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def _process_home_list(
|
|
66
|
+
cls, home_list: list[IntResponseHomeListModel]
|
|
67
|
+
) -> list[VeSyncHome]:
|
|
68
|
+
"""Process the home list and return a list of VeSyncHome instances."""
|
|
69
|
+
homes = []
|
|
70
|
+
for home in home_list:
|
|
71
|
+
if not isinstance(home, IntResponseHomeListModel):
|
|
72
|
+
raise VeSyncAPIResponseError(
|
|
73
|
+
"Invalid home list item type."
|
|
74
|
+
f"Expected IntResponseHomeListModel, got {home}"
|
|
75
|
+
)
|
|
76
|
+
homes.append(VeSyncHome(home.homeId, home.homeName, home.nickname))
|
|
77
|
+
return homes
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
async def build_homes(cls, manager: VeSync) -> bool:
|
|
81
|
+
"""Get home information.
|
|
82
|
+
|
|
83
|
+
This method retrieves the home list from the VeSync API and populates
|
|
84
|
+
the VeSyncHome instances in the manager. The objects are stored in
|
|
85
|
+
`manager.homes` attribute as a list.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
manager (VeSync): The VeSync instance to use for the API call.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
bool: True if the home list was successfully retrieved and populated.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
VeSyncAPIResponseError: If the API response contains an error or
|
|
95
|
+
if the home list is empty.
|
|
96
|
+
"""
|
|
97
|
+
body = cls._build_request_model(manager, RequestHomeModel)
|
|
98
|
+
response, _ = await manager.async_call_api(
|
|
99
|
+
'/cloud/v1/homeManaged/getHomeList',
|
|
100
|
+
method='post',
|
|
101
|
+
json_object=body
|
|
102
|
+
)
|
|
103
|
+
if response is None:
|
|
104
|
+
raise VeSyncAPIResponseError(
|
|
105
|
+
"Response is None, enable debugging to see more information."
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
resp_model = ResponseHomeModel.from_dict(response)
|
|
109
|
+
if resp_model.code != 0:
|
|
110
|
+
error = ErrorCodes.get_error_info(resp_model.code)
|
|
111
|
+
if resp_model.msg is not None:
|
|
112
|
+
error.message = f"{resp_model.msg} ({error.message})"
|
|
113
|
+
raise VeSyncAPIResponseError(
|
|
114
|
+
f"Failed to get home list with error: {error.to_json()}"
|
|
115
|
+
)
|
|
116
|
+
result = resp_model.result
|
|
117
|
+
if not isinstance(result, IntResponseHomeResultModel):
|
|
118
|
+
raise VeSyncAPIResponseError(
|
|
119
|
+
"Error in home list API response."
|
|
120
|
+
f"Expected IntResponseHomeResultModel, got {result}"
|
|
121
|
+
)
|
|
122
|
+
home_list = result.homeList
|
|
123
|
+
if not home_list:
|
|
124
|
+
raise VeSyncAPIResponseError(
|
|
125
|
+
"No homes found in the response."
|
|
126
|
+
)
|
|
127
|
+
for home in home_list:
|
|
128
|
+
if not isinstance(home, IntResponseHomeListModel):
|
|
129
|
+
raise VeSyncAPIResponseError(
|
|
130
|
+
"Invalid home list item type."
|
|
131
|
+
f"Expected IntResponseHomeListModel, got {home}"
|
|
132
|
+
)
|
|
133
|
+
# clear existing homes
|
|
134
|
+
# manager.homes = []
|
|
135
|
+
# manager.homes.append(
|
|
136
|
+
# VeSyncHome(
|
|
137
|
+
# home_id=home.homeId,
|
|
138
|
+
# name=home.homeName,
|
|
139
|
+
# nickname=home.nickname
|
|
140
|
+
# )
|
|
141
|
+
# )
|
|
142
|
+
return True
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class VeSyncRoom:
|
|
146
|
+
"""Class to handle room-related operations in VeSync."""
|
|
147
|
+
|
|
148
|
+
def __init__(self, room_id: str, name: str) -> None:
|
|
149
|
+
"""Initialize the VeSyncRoom instance."""
|
|
150
|
+
self.room_id: str = room_id
|
|
151
|
+
self.name: str = name
|
|
152
|
+
self.devices: list[VeSyncBaseDevice] = []
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def _build_request_model(
|
|
156
|
+
manager: VeSync, request_model: type[RequestBaseModel]
|
|
157
|
+
) -> RequestBaseModel:
|
|
158
|
+
"""Build the request model for room data."""
|
|
159
|
+
req_fields = [field.name for field in fields(request_model) if field.init]
|
|
160
|
+
body = Helpers.get_class_attributes(DefaultValues, req_fields)
|
|
161
|
+
body.update(Helpers.get_class_attributes(manager, req_fields))
|
|
162
|
+
return request_model(**body)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyvesync
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.0b2
|
|
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>
|
|
@@ -340,7 +340,7 @@ manager.devices.outlets = [VeSyncOutletInstances]
|
|
|
340
340
|
manager.devices.switches = [VeSyncSwitchInstances]
|
|
341
341
|
manager.devices.fans = [VeSyncFanInstances]
|
|
342
342
|
manager.devices.bulbs = [VeSyncBulbInstances]
|
|
343
|
-
manager.devices.
|
|
343
|
+
manager.devices.air_purifiers = [VeSyncPurifierInstances]
|
|
344
344
|
manager.devices.humidifiers = [VeSyncHumidifierInstances]
|
|
345
345
|
manager.devices.air_fryers = [VeSyncAirFryerInstances]
|
|
346
346
|
managers.devices.thermostats = [VeSyncThermostatInstances]
|
|
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
|