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.
Files changed (58) hide show
  1. {pyvesync-3.0.0b0/src/pyvesync.egg-info → pyvesync-3.0.0b2}/PKG-INFO +2 -2
  2. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/README.md +1 -1
  3. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/pyproject.toml +1 -1
  4. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/base_devices/fan_base.py +13 -22
  5. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/base_devices/humidifier_base.py +19 -22
  6. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/base_devices/outlet_base.py +7 -7
  7. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/base_devices/purifier_base.py +18 -23
  8. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/base_devices/switch_base.py +5 -5
  9. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/base_devices/vesyncbasedevice.py +6 -5
  10. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/const.py +6 -6
  11. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/device_container.py +1 -1
  12. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/device_map.py +1 -1
  13. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/devices/vesynchumidifier.py +5 -4
  14. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/devices/vesyncpurifier.py +9 -5
  15. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/models/fan_models.py +1 -1
  16. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/models/home_models.py +8 -8
  17. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/models/humidifier_models.py +2 -2
  18. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/models/purifier_models.py +7 -8
  19. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/models/vesync_models.py +63 -12
  20. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/utils/errors.py +1 -1
  21. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/utils/helpers.py +0 -2
  22. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/utils/logs.py +6 -1
  23. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/vesync.py +73 -4
  24. pyvesync-3.0.0b2/src/pyvesync/vesynchome.py +162 -0
  25. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2/src/pyvesync.egg-info}/PKG-INFO +2 -2
  26. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync.egg-info/SOURCES.txt +1 -0
  27. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/LICENSE +0 -0
  28. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/MANIFEST.in +0 -0
  29. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/requirements.txt +0 -0
  30. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/setup.cfg +0 -0
  31. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/setup.py +0 -0
  32. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/__init__.py +0 -0
  33. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/base_devices/__init__.py +0 -0
  34. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/base_devices/bulb_base.py +0 -0
  35. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/base_devices/fryer_base.py +0 -0
  36. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/base_devices/thermostat_base.py +0 -0
  37. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/devices/__init__.py +0 -0
  38. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/devices/vesyncbulb.py +0 -0
  39. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/devices/vesyncfan.py +0 -0
  40. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/devices/vesynckitchen.py +0 -0
  41. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/devices/vesyncoutlet.py +0 -0
  42. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/devices/vesyncswitch.py +0 -0
  43. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/devices/vesyncthermostat.py +0 -0
  44. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/models/__init__.py +0 -0
  45. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/models/base_models.py +0 -0
  46. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/models/bulb_models.py +0 -0
  47. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/models/bypass_models.py +0 -0
  48. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/models/fryer_models.py +0 -0
  49. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/models/outlet_models.py +0 -0
  50. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/models/switch_models.py +0 -0
  51. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/models/thermostat_models.py +0 -0
  52. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/utils/__init__.py +0 -0
  53. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/utils/colors.py +0 -0
  54. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/utils/device_mixins.py +0 -0
  55. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync/utils/enum_utils.py +0 -0
  56. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync.egg-info/dependency_links.txt +0 -0
  57. {pyvesync-3.0.0b0 → pyvesync-3.0.0b2}/src/pyvesync.egg-info/requires.txt +0 -0
  58. {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.0b0
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.purifiers = [VeSyncPurifierInstances]
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.purifiers = [VeSyncPurifierInstances]
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.0b0"
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. Attributes are set to
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 = FanSleepPreference.UNKNOWN
97
- self.sleep_fallasleep_remain: str | None = DeviceStatus.UNKNOWN
98
- self.sleep_oscillation_switch: str | None = DeviceStatus.UNKNOWN
99
- self.sleep_change_fan_level: str | None = DeviceStatus.UNKNOWN
100
- self.mute_status: str = DeviceStatus.UNKNOWN
101
- self.mute_set_state: str = DeviceStatus.UNKNOWN
102
- self.oscillation_status: str = DeviceStatus.UNKNOWN
103
- self.oscillation_set_state: str = DeviceStatus.UNKNOWN
104
- self.display_status: str = DeviceStatus.UNKNOWN
105
- self.display_set_state: str = DeviceStatus.UNKNOWN
106
- self.displaying_type: str = StrFlag.NOT_SUPPORTED
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, they should be added directly to
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 = IntFlag.NOT_SUPPORTED
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 = IntFlag.NOT_SUPPORTED
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 = HumidifierModes.UNKNOWN
109
- self.nightlight_brightness: int = IntFlag.NOT_SUPPORTED
110
- self.nightlight_status: str = StrFlag.NOT_SUPPORTED
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 = IntFlag.NOT_SUPPORTED
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 = IntFlag.NOT_SUPPORTED
112
+ self.temperature: int | None = None
116
113
  # Superior 6000S States
117
- self.auto_preference: int = IntFlag.NOT_SUPPORTED
118
- self.filter_life_percent: int = IntFlag.NOT_SUPPORTED
119
- self.drying_mode_level: int = IntFlag.NOT_SUPPORTED
120
- self.drying_mode_auto_switch: str = StrFlag.NOT_SUPPORTED
121
- self.drying_mode_status: str = StrFlag.NOT_SUPPORTED
122
- self.drying_mode_time_remain: int = IntFlag.NOT_SUPPORTED
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, IntFlag, StrFlag, NightlightModes
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 = IntFlag.NOT_SUPPORTED
93
- self.energy: float = IntFlag.NOT_SUPPORTED
94
- self.voltage: float = IntFlag.NOT_SUPPORTED
95
- self.nightlight_status: str | None = StrFlag.NOT_SUPPORTED
96
- self.nightlight_brightness: int | None = IntFlag.NOT_SUPPORTED
97
- self.nightlight_automode: str | None = StrFlag.NOT_SUPPORTED
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 deprecated import deprecated
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 = PurifierAutoPreference.UNKNOWN
97
+ self.auto_preference_type: str | None = None
101
98
  self.auto_room_size: int | None = None
102
- self._air_quality_level: AirQualityLevel = AirQualityLevel.UNKNOWN
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 = DeviceStatus.UNKNOWN
106
- self.display_set_state: str = DeviceStatus.UNKNOWN
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
- # When update is first called, devices that support these attributes will
110
- # update them from NOT_SUPPORTED, whether to None or another value.
111
- self.pm25: int | None = IntFlag.NOT_SUPPORTED
112
- self.pm1: int | None = IntFlag.NOT_SUPPORTED
113
- self.pm10: int | None = IntFlag.NOT_SUPPORTED
114
- self.aq_percent: int | None = IntFlag.NOT_SUPPORTED
115
- self.light_detection_switch: str = StrFlag.NOT_SUPPORTED
116
- self.light_detection_status: str = StrFlag.NOT_SUPPORTED
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, StrFlag, IntFlag
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 | str | None = StrFlag.NOT_SUPPORTED
61
- self.brightness: int = IntFlag.NOT_SUPPORTED
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 = StrFlag.NOT_SUPPORTED
64
- self.indicator_status: str = StrFlag.NOT_SUPPORTED
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, IntFlag, StrFlag
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 False
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 uuid1
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 = "2.8.6"
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 3.0.51"
45
+ BYPASS_APP_V = f"VeSync {APP_VERSION}"
46
46
  BYPASS_HEADER_UA = "okhttp/3.12.1"
47
- TERMINAL_ID = '2' + uuid1().hex
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 = 0) -> bool:
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, 3)),
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, IntFlag, ConnectionStatus
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 or IntFlag.NOT_SUPPORTED
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 != IntFlag.NOT_SUPPORTED
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 != IntFlag.NOT_SUPPORTED:
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 != StrFlag.NOT_SUPPORTED:
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 != IntFlag.NOT_SUPPORTED:
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.levelNew
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
  }
@@ -33,7 +33,7 @@ class TowerFanResult(BypassV2InnerResult):
33
33
  thermalComfort: int
34
34
  errorCode: int
35
35
  scheduleCount: int
36
- displayingType: int = IntFlag.NOT_SUPPORTED
36
+ displayingType: int | None = None
37
37
  sleepPreference: FanSleepPreferences | None = None
38
38
 
39
39
 
@@ -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 = field(init=False)
40
- token: str = field(init=False)
41
- userCountryCode: str = field(init=False)
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(ResponseBaseModel):
54
+ class ResponseHomeModel(ResponseCodeModel):
56
55
  """Model for the home data response.
57
56
 
58
- Inherits from `ResponseBaseModel`. The `ResponseBaseModel` class provides the
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
- homeID: str
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 = IntFlag.NOT_SUPPORTED
77
- night_light_brightness: int = IntFlag.NOT_SUPPORTED
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 = IntFlag.NOT_SUPPORTED
57
- filterOpenState: int | None = IntFlag.NOT_SUPPORTED
58
- PM1: int | None = IntFlag.NOT_SUPPORTED
59
- PM10: int | None = IntFlag.NOT_SUPPORTED
60
- AQPercent: int | None = IntFlag.NOT_SUPPORTED
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 = StrFlag.NOT_SUPPORTED
85
- fan_rotate: str | None = StrFlag.NOT_SUPPORTED
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
- appVersion: str = DefaultValues.appVersion
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
- phoneBrand: str = DefaultValues.phoneBrand
37
- phoneOS: str = DefaultValues.phoneOS
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 inherited by the `ResponseLoginModel` class.
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 = 0
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):
@@ -38,8 +38,6 @@ T_MODEL = TypeVar('T_MODEL', bound=DataClassORJSONMixin)
38
38
 
39
39
  _LOGGER = logging.getLogger(__name__)
40
40
 
41
- API_BASE_URL = 'https://smartapi.vesync.com'
42
- API_RATE_LIMIT = 30
43
41
  NUMERIC_OPT = Union[float, str, None]
44
42
 
45
43
  NUMERIC_STRICT = Union[float, str]
@@ -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
- file_handler = logging.FileHandler(file_name)
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='login',
305
+ method='appLoginV3',
303
306
  password=self.password,
304
307
  )
305
308
  resp_dict, _ = await self.async_call_api(
306
- '/cloud/v1/user/login', 'post',
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.0b0
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.purifiers = [VeSyncPurifierInstances]
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]
@@ -10,6 +10,7 @@ src/pyvesync/const.py
10
10
  src/pyvesync/device_container.py
11
11
  src/pyvesync/device_map.py
12
12
  src/pyvesync/vesync.py
13
+ src/pyvesync/vesynchome.py
13
14
  src/pyvesync.egg-info/PKG-INFO
14
15
  src/pyvesync.egg-info/SOURCES.txt
15
16
  src/pyvesync.egg-info/dependency_links.txt
File without changes
File without changes
File without changes
File without changes
File without changes