pyvesync 3.4.1__tar.gz → 3.5.0__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 (64) hide show
  1. {pyvesync-3.4.1/src/pyvesync.egg-info → pyvesync-3.5.0}/PKG-INFO +165 -153
  2. {pyvesync-3.4.1 → pyvesync-3.5.0}/README.md +161 -149
  3. {pyvesync-3.4.1 → pyvesync-3.5.0}/pyproject.toml +4 -4
  4. pyvesync-3.5.0/requirements.txt +2 -0
  5. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/auth.py +43 -28
  6. pyvesync-3.5.0/src/pyvesync/base_devices/fryer_base.py +1337 -0
  7. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/base_devices/humidifier_base.py +50 -0
  8. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/base_devices/outlet_base.py +18 -0
  9. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/const.py +527 -9
  10. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/device_container.py +11 -2
  11. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/device_map.py +766 -24
  12. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/devices/vesynchumidifier.py +192 -3
  13. pyvesync-3.5.0/src/pyvesync/devices/vesynckitchen.py +1089 -0
  14. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/devices/vesyncoutlet.py +10 -0
  15. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/devices/vesyncthermostat.py +2 -2
  16. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/models/bypass_models.py +2 -1
  17. pyvesync-3.5.0/src/pyvesync/models/fryer_models.py +312 -0
  18. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/models/humidifier_models.py +15 -0
  19. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/utils/colors.py +127 -0
  20. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/utils/errors.py +47 -23
  21. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/utils/helpers.py +10 -0
  22. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/vesync.py +32 -7
  23. {pyvesync-3.4.1 → pyvesync-3.5.0/src/pyvesync.egg-info}/PKG-INFO +165 -153
  24. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync.egg-info/requires.txt +3 -3
  25. pyvesync-3.4.1/requirements.txt +0 -3
  26. pyvesync-3.4.1/src/pyvesync/base_devices/fryer_base.py +0 -68
  27. pyvesync-3.4.1/src/pyvesync/devices/vesynckitchen.py +0 -653
  28. pyvesync-3.4.1/src/pyvesync/models/fryer_models.py +0 -34
  29. {pyvesync-3.4.1 → pyvesync-3.5.0}/LICENSE +0 -0
  30. {pyvesync-3.4.1 → pyvesync-3.5.0}/MANIFEST.in +0 -0
  31. {pyvesync-3.4.1 → pyvesync-3.5.0}/setup.cfg +0 -0
  32. {pyvesync-3.4.1 → pyvesync-3.5.0}/setup.py +0 -0
  33. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/__init__.py +0 -0
  34. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/base_devices/__init__.py +0 -0
  35. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/base_devices/bulb_base.py +0 -0
  36. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/base_devices/fan_base.py +0 -0
  37. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/base_devices/purifier_base.py +0 -0
  38. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/base_devices/switch_base.py +0 -0
  39. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/base_devices/thermostat_base.py +0 -0
  40. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/base_devices/vesyncbasedevice.py +0 -0
  41. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/devices/__init__.py +0 -0
  42. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/devices/vesyncbulb.py +0 -0
  43. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/devices/vesyncfan.py +0 -0
  44. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/devices/vesyncpurifier.py +0 -0
  45. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/devices/vesyncswitch.py +0 -0
  46. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/models/__init__.py +0 -0
  47. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/models/base_models.py +0 -0
  48. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/models/bulb_models.py +0 -0
  49. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/models/fan_models.py +0 -0
  50. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/models/home_models.py +0 -0
  51. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/models/outlet_models.py +0 -0
  52. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/models/purifier_models.py +0 -0
  53. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/models/switch_models.py +0 -0
  54. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/models/thermostat_models.py +0 -0
  55. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/models/vesync_models.py +0 -0
  56. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/py.typed +0 -0
  57. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/utils/__init__.py +0 -0
  58. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/utils/device_mixins.py +0 -0
  59. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/utils/enum_utils.py +0 -0
  60. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/utils/logs.py +0 -0
  61. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync/vesynchome.py +0 -0
  62. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync.egg-info/SOURCES.txt +0 -0
  63. {pyvesync-3.4.1 → pyvesync-3.5.0}/src/pyvesync.egg-info/dependency_links.txt +0 -0
  64. {pyvesync-3.4.1 → pyvesync-3.5.0}/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.4.1
3
+ Version: 3.5.0
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>
@@ -15,7 +15,7 @@ Classifier: Topic :: Home Automation
15
15
  Requires-Python: >=3.11
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
- Requires-Dist: aiohttp>=3.8.1
18
+ Requires-Dist: aiohttp>=3.14.4
19
19
  Requires-Dist: mashumaro[orjson]>=3.13.1
20
20
  Provides-Extra: dev
21
21
  Requires-Dist: pytest; extra == "dev"
@@ -23,7 +23,7 @@ Requires-Dist: pytest-cov; extra == "dev"
23
23
  Requires-Dist: pyyaml; extra == "dev"
24
24
  Requires-Dist: pylint; extra == "dev"
25
25
  Requires-Dist: mypy; extra == "dev"
26
- Requires-Dist: ruff; extra == "dev"
26
+ Requires-Dist: ruff==0.15.21; extra == "dev"
27
27
  Requires-Dist: pre-commit; extra == "dev"
28
28
  Requires-Dist: tox; extra == "dev"
29
29
  Requires-Dist: requests; extra == "dev"
@@ -31,7 +31,7 @@ Requires-Dist: aiofiles; extra == "dev"
31
31
  Provides-Extra: docs
32
32
  Requires-Dist: mkdocstrings-python; extra == "docs"
33
33
  Requires-Dist: mkdocs; extra == "docs"
34
- Requires-Dist: ruff; extra == "docs"
34
+ Requires-Dist: ruff==0.15.21; extra == "docs"
35
35
  Requires-Dist: Pygments; extra == "docs"
36
36
  Requires-Dist: mkdocs-material; extra == "docs"
37
37
  Requires-Dist: mkdocstrings; extra == "docs"
@@ -46,13 +46,13 @@ Requires-Dist: griffe-inherited-docstrings; extra == "docs"
46
46
  Requires-Dist: griffe_warnings_deprecated; extra == "docs"
47
47
  Dynamic: license-file
48
48
 
49
- # pyvesync [![build status](https://img.shields.io/pypi/v/pyvesync.svg)](https://pypi.python.org/pypi/pyvesync) [![Build Status](https://dev.azure.com/webdjoe/pyvesync/_apis/build/status/webdjoe.pyvesync?branchName=master)](https://dev.azure.com/webdjoe/pyvesync/_build/latest?definitionId=4&branchName=master) [![Open Source? Yes!](https://badgen.net/badge/Open%20Source%20%3F/Yes%21/blue?icon=github)](https://github.com/Naereen/badges/) [![PyPI license](https://img.shields.io/pypi/l/ansicolortags.svg)](https://pypi.python.org/pypi/ansicolortags/) <!-- omit in toc -->
49
+ # pyvesync [![build status](https://img.shields.io/pypi/v/pyvesync.svg)](https://pypi.python.org/pypi/pyvesync) [![Open Source? Yes!](https://badgen.net/badge/Open%20Source%20%3F/Yes%21/blue?icon=github)](https://github.com/Naereen/badges/) [![PyPI license](https://img.shields.io/pypi/l/ansicolortags.svg)](https://pypi.python.org/pypi/ansicolortags/) <!-- omit in toc -->
50
50
 
51
51
  pyvesync is a library to manage VeSync compatible [smart home devices](#supported-devices)
52
52
 
53
53
  <a href="https://webdjoe.github.io/pyvesync/latest"><img src="./docs/assets/docs.svg"></a>
54
54
 
55
- **Check out the new [pyvesync documentation](https://webdjoe.github.io/pyvesync/) for usage and full API details.**
55
+ **Check out the new [pyvesync documentation](https://webdjoe.github.io/pyvesync/latest/) for usage and full API details.**
56
56
 
57
57
  ## Supported Product Types
58
58
 
@@ -67,7 +67,7 @@ pyvesync is a library to manage VeSync compatible [smart home devices](#supporte
67
67
 
68
68
  See the [supported devices](https://webdjoe.github.io/pyvesync/latest/supported_devices/) page for a complete list of supported devices and device types.
69
69
 
70
- ## What's new in pyvesync 3.0
70
+ ## pyvesync v3 Features
71
71
 
72
72
  **BREAKING CHANGES** - The release of pyvesync 3.0 comes with many improvements and new features, but as a result there are many breaking changes. The structure has been completely refactored, so please read through the README and thoroughly test before deploying.
73
73
 
@@ -83,7 +83,6 @@ Some of the changes are:
83
83
  - Implemented custom exceptions and error (code) handling for API responses.
84
84
  - `const` module to hold all library constants
85
85
  - Built the `DeviceMap` class to hold the mapping and features of devices.
86
- - COMING SOON: Use API to pull device modes and operating features.
87
86
 
88
87
  See [pyvesync V3](https://webdjoe.github.io/pyvesync/latest/pyvesync3/) for more information on the changes.
89
88
 
@@ -94,7 +93,7 @@ Library is now asynchronous, using aiohttp as a replacement for requests. The `p
94
93
  ```python
95
94
  import asyncio
96
95
  import aiohttp
97
- from pyvesync.vesync import VeSync
96
+ from pyvesync import VeSync
98
97
 
99
98
  async def main():
100
99
  async with VeSync(
@@ -109,9 +108,6 @@ async def main():
109
108
  # Redact mode is enabled by default, set to False to disable
110
109
  manager.redact = False
111
110
 
112
- # To print request & response content for all API calls enable verbose mode
113
- manager.verbose = True
114
-
115
111
  # To print logs to file
116
112
  manager.log_to_file("pyvesync.log")
117
113
 
@@ -119,17 +115,17 @@ async def main():
119
115
  if not manager.enabled:
120
116
  print("Not logged in.")
121
117
  return
122
- await manager.get_devices() # Instantiates supported devices in device list, automatically called by login, only needed if you would like updates
118
+ await manager.get_devices() # Instantiates supported devices in device list
123
119
  await manager.update() # Updates the state of all devices
124
120
 
125
121
  # manager.devices is a DeviceContainer object
126
122
  # manager.devices.outlets is a list of VeSyncOutlet objects
127
123
  # manager.devices.switches is a list of VeSyncSwitch objects
128
- # manager.devices.fans is a list of VeSyncFan objects
124
+ # manager.devices.fans is a list of VeSyncFanBase objects
129
125
  # manager.devices.bulbs is a list of VeSyncBulb objects
130
- # manager.devices.humidifiers is a list of VeSyncHumid objects
131
- # manager.devices.air_purifiers is a list of VeSyncAir objects
132
- # manager.devices.air_fryers is a list of VeSyncAirFryer objects
126
+ # manager.devices.humidifiers is a list of VeSyncHumidifier objects
127
+ # manager.devices.air_purifiers is a list of VeSyncPurifier objects
128
+ # manager.devices.air_fryers is a list of VeSyncFryer objects
133
129
  # manager.devices.thermostats is a list of VeSyncThermostat objects
134
130
 
135
131
  for outlet in manager.devices.outlets:
@@ -140,8 +136,8 @@ async def main():
140
136
 
141
137
  # State of object held in `device.state` attribute
142
138
  print(outlet.state)
143
- state_json = outlet.dumps() # Returns JSON string of device state
144
- state_bytes = orjson.dumps(outlet.state) # Returns bytes of device state
139
+ state_json = outlet.to_json(state=True) # Returns JSON string of device state
140
+ state_bytes = outlet.to_jsonb(state=True) # Returns JSON bytes of device state
145
141
 
146
142
  # to view the response information of the last API call
147
143
  print(outlet.last_response)
@@ -153,7 +149,7 @@ async def main():
153
149
  session = aiohttp.ClientSession()
154
150
 
155
151
  async def main():
156
- async with VeSync("user", "password", session=session):
152
+ async with VeSync("user", "password", session=session) as manager:
157
153
  await manager.login()
158
154
  await manager.update()
159
155
 
@@ -166,7 +162,7 @@ if __name__ == "__main__":
166
162
  If using `async with` is not ideal, the `__aenter__()` and `__aexit__()` methods need to be called manually:
167
163
 
168
164
  ```python
169
- manager = VeSync(user, password)
165
+ manager = VeSync("user", "password")
170
166
 
171
167
  await manager.__aenter__()
172
168
 
@@ -175,7 +171,7 @@ await manager.__aenter__()
175
171
  await manager.__aexit__(None, None, None)
176
172
  ```
177
173
 
178
- pvesync will close the `ClientSession` that was created by the library on `__aexit__`. If a session is passed in as an argument, the library does not close it. If a session is passed in and not closed, aiohttp will generate an error on exit:
174
+ pyvesync will close the `ClientSession` that was created by the library on `__aexit__`. If a session is passed in as an argument, the library does not close it. If a session is passed in and not closed, aiohttp will generate an error on exit:
179
175
 
180
176
  ```text
181
177
  2025-02-16 14:41:07 - ERROR - asyncio - Unclosed client session
@@ -190,9 +186,9 @@ The VeSync signature is:
190
186
  VeSync(
191
187
  username: str,
192
188
  password: str,
193
- country_code: str = DEFAULT_COUNTRY_CODE, # US
189
+ country_code: str = DEFAULT_REGION, # "US"
194
190
  session: ClientSession | None = None,
195
- time_zone: str = DEFAULT_TZ # America/New_York,
191
+ time_zone: str = DEFAULT_TZ, # "America/New_York"
196
192
  redact: bool = True,
197
193
  )
198
194
  ```
@@ -208,7 +204,8 @@ There is a new nomenclature for product types that defines the device class. The
208
204
  4. `purifier` - Air purifiers (not humidifiers)
209
205
  5. `humidifier` - Humidifiers (not air purifiers)
210
206
  6. `bulb` - Light bulbs (not dimmers or switches)
211
- 7. `airfryer` - Air fryers
207
+ 7. `air fryer` - Air fryers
208
+ 8. `thermostat` - Thermostats
212
209
 
213
210
  See [Supported Devices](#supported-devices) for a complete list of supported devices and models.
214
211
 
@@ -218,22 +215,22 @@ Exceptions are no longer caught by the library and must be handled by the user.
218
215
 
219
216
  Errors that occur at the aiohttp level are raised automatically and propagated to the user. That means exceptions raised by aiohttp that inherit from `aiohttp.ClientError` are propagated.
220
217
 
221
- When the connection to the VeSync API succeeds but returns an error code that prevents the library from functioning a custom exception inherited from `pyvesync.logs.VeSyncError` is raised.
218
+ When the connection to the VeSync API succeeds but returns an error code that prevents the library from functioning a custom exception inherited from `pyvesync.utils.errors.VeSyncError` is raised.
222
219
 
223
220
  Custom Exceptions raised by all API calls:
224
221
 
225
- - `pyvesync.logs.VeSyncServerError` - The API connected and returned a code indicated there is a server-side error.
226
- - `pyvesync.logs.VeSyncRateLimitError` - The API's rate limit has been exceeded.
227
- - `pyvesync.logs.VeSyncAPIStatusCodeError` - The API returned a non-200 status code.
228
- - `pyvesync.logs.VeSyncAPIResponseError` - The response from the API was not in an expected format.
222
+ - `pyvesync.utils.errors.VeSyncServerError` - The API connected and returned a code indicated there is a server-side error.
223
+ - `pyvesync.utils.errors.VeSyncRateLimitError` - The API's rate limit has been exceeded.
224
+ - `pyvesync.utils.errors.VeSyncAPIStatusCodeError` - The API returned a non-200 status code.
225
+ - `pyvesync.utils.errors.VeSyncAPIResponseError` - The response from the API was not in an expected format.
229
226
 
230
227
  Login API Exceptions
231
228
 
232
- - `pyvesync.logs.VeSyncLoginError` - The username or password is incorrect.
229
+ - `pyvesync.utils.errors.VeSyncLoginError` - The username or password is incorrect.
233
230
 
234
- See [errors](https://webdjoe.github.io/pyvesync/latest/development/utils/errors) documentation for a complete list of error codes and exceptions.
231
+ See the [errors](https://webdjoe.github.io/pyvesync/latest/development/utils/errors/) documentation for a complete list of error codes and exceptions.
235
232
 
236
- The [raise_api_errors()](https://webdjoe.github.io/pyvesync/latest/development/utils/errors/#pyvesync.utils.errors.raise_api_errors) function is called for every API call and checks for general response errors. It can raise the following exceptions:
233
+ The `raise_api_errors()` function is called for every API call and checks for general response errors. It can raise the following exceptions:
237
234
 
238
235
  - `VeSyncServerError` - The API connected and returned a code indicated there is a server-side error.
239
236
  - `VeSyncRateLimitError` - The API's rate limit has been exceeded.
@@ -257,12 +254,15 @@ pip install pyvesync
257
254
 
258
255
  ### Etekcity Outlets
259
256
 
260
- 1. Voltson Smart WiFi Outlet- Round (7A model ESW01-USA)
257
+ 1. Voltson Smart WiFi Outlet - Round (7A model ESW01-USA)
261
258
  2. Voltson Smart WiFi Outlet - Round (10A model ESW01-EU)
262
- 3. Voltson Smart Wifi Outlet - Round (10A model ESW03-USA)
263
- 4. Voltson Smart Wifi Outlet - Round (10A model ESW10-USA)
259
+ 3. Voltson Smart WiFi Outlet - Round (10A model ESW03-USA)
260
+ 4. Voltson Smart WiFi Outlet - Round (10A model ESW10-USA)
264
261
  5. Voltson Smart WiFi Outlet - Rectangle (15A model ESW15-USA)
265
262
  6. Two Plug Outdoor Outlet (ESO15-TB) (Each plug is a separate `VeSyncOutlet` object, energy readings are for both plugs combined)
263
+ 7. BSDOG / Greensun Smart Outlet Series (BSDOG01, BSDOG02, WYSMTOD16A, WM-PLUG and more)
264
+ 8. WHOGPLUG / Greensun Smart Plug
265
+ 9. WYLDR Smart Plug (WYLDR16A1081) (Supports energy monitoring but not energy history)
266
266
 
267
267
  <!--SUPPORTED OUTLETS END-->
268
268
 
@@ -271,46 +271,58 @@ pip install pyvesync
271
271
  ### Wall Switches
272
272
 
273
273
  1. Etekcity Smart WiFi Light Switch (model ESWL01)
274
- 2. Etekcity Wifi Dimmer Switch (ESD16)
274
+ 2. Etekcity WiFi 3-Way Switch (model ESWL03)
275
+ 3. Etekcity WiFi Dimmer Switch (model ESWD16)
275
276
 
276
277
  <!--SUPPORTED SWITCHES END-->
277
278
 
278
279
  ### Levoit Air Purifiers
279
280
 
280
281
  1. LV-PUR131S
281
- 2. Core 200S
282
- 3. Core 300S
283
- 4. Core 400S
284
- 5. Core 600S
285
- 6. Vital 100S
286
- 7. Vital 200S
282
+ 2. LV-RH131S
283
+ 3. Core 200S
284
+ 4. Core 300S
285
+ 5. Core 400S
286
+ 6. Core 600S
287
+ 7. Vital 100S / 200S
287
288
  8. Everest Air
289
+ 9. Sprout Air Purifier
288
290
 
289
- ### Etekcity Bulbs
291
+ ### Etekcity / Valceno Bulbs
290
292
 
291
293
  1. Soft White Dimmable Smart Bulb (ESL100)
292
294
  2. Cool to Soft White Tunable Dimmable Bulb (ESL100CW)
293
-
294
- ### Valceno Bulbs
295
-
296
- 1. Valceno Multicolor Bulb (XYD0001)
295
+ 3. Multicolor Dimmable Bulb (ESL100MC)
296
+ 4. Valceno Multicolor Bulb (XYD0001)
297
297
 
298
298
  ### Levoit Humidifiers
299
299
 
300
- 1. Dual 200S
301
- 2. Classic 300S
302
- 3. LV600S
303
- 4. OasisMist 450S
304
- 5. OasisMist 600S
300
+ 1. Classic 200S
301
+ 2. Dual 200S
302
+ 3. Classic 300S
303
+ 4. LV600S / LV603S
304
+ 5. OasisMist 450S / 600S
305
305
  6. OasisMist 1000S
306
+ 7. Superior 6000S
307
+ 8. Sprout Humidifier
308
+
309
+ ### Levoit Fans
306
310
 
307
- ### Cosori Air Fryer
311
+ 1. 42 in. Tower Fan (LTF-F422S Series)
312
+ 2. Pedestal Fan (LPF-R432S Series)
308
313
 
309
- 1. Cosori 3.7 and 5.8 Quart Air Fryer
314
+ ### Cosori Air Fryers
310
315
 
311
- ### Fans
316
+ 1. Cosori 3.7 Quart Air Fryer (CS137-AF)
317
+ 2. Cosori 5.8 Quart Air Fryer (CS158-AF)
318
+ 3. Cosori TurboBlaze 6 Quart Air Fryer (CAF-DC601S)
319
+ 4. Cosori Dual Blaze 6.8 Quart Air Fryer (CAF-P583S)
320
+ 5. Cosori Dual Air Fryer (CAF-TF101S)
321
+ 6. Cosori Turbo Tower Pro Smart Air Fryer (CAF-DC111S-AEU)
312
322
 
313
- 1. 42 in. Tower Fan
323
+ ### Thermostats
324
+
325
+ 1. Aura Thermostat (LTM-A401S-WUS)
314
326
 
315
327
  <!--SUPPORTED DEVICES END-->
316
328
 
@@ -319,10 +331,6 @@ pip install pyvesync
319
331
  ```python
320
332
  import asyncio
321
333
  from pyvesync import VeSync
322
- from pyvesync.logs import VeSyncLoginError
323
-
324
- # VeSync is an asynchronous context manager
325
- # VeSync(username, password, redact=True, session=None)
326
334
 
327
335
  async def main():
328
336
  async with VeSync("user", "password") as manager:
@@ -332,14 +340,14 @@ async def main():
332
340
  # Acts as a set of device instances
333
341
  device_container = manager.devices
334
342
 
335
- outlets = device_container.outlets # List of outlet instances
343
+ outlets = device_container.outlets # List of outlet instances
336
344
  outlet = outlets[0]
337
345
  await outlet.update()
338
346
  await outlet.turn_off()
339
347
  outlet.display()
340
348
 
341
- # Iterate of entire device list
342
- for devices in device_container:
349
+ # Iterate over entire device list
350
+ for device in device_container:
343
351
  device.display()
344
352
 
345
353
 
@@ -347,52 +355,54 @@ if __name__ == "__main__":
347
355
  asyncio.run(main())
348
356
  ```
349
357
 
350
- If you want to reuse your token and account_id between runs. The `VeSync.auth` object holds the credentials and helper methods to save and load credentials. See the [Authentication Documentation](https://webdjoe.github.io/pyvesync/latest/authentication) for more details.
358
+ If you want to reuse your token and account_id between runs. The `VeSync.auth` object holds the credentials and helper methods to save and load credentials. See the [Authentication Documentation](https://webdjoe.github.io/pyvesync/latest/authentication) for more details.
351
359
 
352
360
  ```python
353
361
  import asyncio
354
362
  from pyvesync import VeSync
355
- from pyvesync.logs import VeSyncLoginError
356
-
357
- # VeSync is an asynchronous context manager
358
- # VeSync(username, password, redact=True, session=None)
359
363
 
360
364
  async def main():
361
365
  async with VeSync("user", "password") as manager:
362
366
 
363
367
  # If credentials are stored in a file, it can be loaded
364
- # the default location is ~/.vesync_token
368
+ # with no path, .vesync_auth is read from the current directory,
369
+ # then from the home directory
365
370
  await manager.load_credentials_from_file()
366
371
  # or the file path can be passed
367
372
  await manager.load_credentials_from_file("/path/to/token_file")
368
373
 
369
374
  # Or credentials can be passed directly
370
- manager.set_credentials("your_token", "your_account_id")
371
-
372
- # No login needed
375
+ manager.set_credentials(
376
+ token="your_token",
377
+ account_id="your_account_id",
378
+ country_code="US",
379
+ region="US",
380
+ )
381
+
382
+ # No login needed if token is valid
373
383
  # await manager.login()
374
384
 
375
385
  # To store credentials to a file after login
376
- await manager.save_credentials() # Saves to default location ~/.vesync_token
377
- # or pass a file path
378
386
  await manager.save_credentials("/path/to/token_file")
379
387
 
380
- # Output Credentials as JSON String
381
- await manager.output_credentials()
388
+ # Output credentials as JSON string
389
+ print(manager.output_credentials_json())
390
+ # Output credentials as dictionary
391
+ print(manager.output_credentials_dict())
382
392
 
383
393
  await manager.update()
384
394
 
385
395
  # Acts as a set of device instances
386
396
  device_container = manager.devices
387
397
 
388
- outlets = device_container.outlets # List of outlet instances
398
+ outlets = device_container.outlets # List of outlet instances
389
399
  outlet = outlets[0]
390
400
  await outlet.update()
391
401
  await outlet.turn_off()
392
402
  outlet.display()
393
403
 
394
- # Iterate of entire device list
395
- for devices in device_container:
404
+ # Iterate over entire device list
405
+ for device in device_container:
396
406
  device.display()
397
407
 
398
408
 
@@ -417,7 +427,7 @@ manager.devices.bulbs = [VeSyncBulbInstances]
417
427
  manager.devices.air_purifiers = [VeSyncPurifierInstances]
418
428
  manager.devices.humidifiers = [VeSyncHumidifierInstances]
419
429
  manager.devices.air_fryers = [VeSyncAirFryerInstances]
420
- managers.devices.thermostats = [VeSyncThermostatInstances]
430
+ manager.devices.thermostats = [VeSyncThermostatInstances]
421
431
 
422
432
  # Get device by device name
423
433
  dev_name = "My Device"
@@ -437,30 +447,35 @@ See the [device documentation](https://webdjoe.github.io/pyvesync/latest/devices
437
447
 
438
448
  ## Debug mode and redact
439
449
 
440
- To set debug, use the `logger` object in the `VeSync` class. Setting the logger level to `DEBUG` will print debug information to the console. The `log_to_file()` method can be used to log to a file.
450
+ There are three independent knobs that control logging output:
441
451
 
442
- The `redact` argument removes any tokens and account identifiers from the output to allow for easier sharing. The `redact` argument has no impact if `debug` is not `True`.
452
+ - **Debug logging** - set the `pyvesync` logger level to `DEBUG`. The read-only `manager.debug` property reflects whether debug logging is currently active.
453
+ - **`redact`** - the `redact` argument (default `True`) replaces tokens, account IDs, and other sensitive values (`token`, `password`, `email`, `accountId`, `authKey`, `uuid`, `cid`, `authorizeCode`) with `##_REDACTED_##` so logs can be shared safely. It only has an effect while debug logging is enabled.
454
+ - **`LibraryLogger.verbose`** - set this class attribute to `True` to additionally print the full request & response body of every API call.
443
455
 
444
- This is an example of debug mode with redact enabled:
456
+ ### Debug to the console
457
+
458
+ Setting the `pyvesync` logger to `DEBUG` sends debug output to wherever your logging is configured (the console by default). `redact` stays on so tokens are not exposed:
445
459
 
446
460
  ```python
447
461
  import logging
448
462
  import asyncio
449
- import aiohttp
450
- from pyvesync.vesync import VeSync
463
+ from pyvesync import VeSync
464
+ from pyvesync.utils.logs import LibraryLogger
451
465
 
452
466
  logger = logging.getLogger("pyvesync")
453
467
  logger.setLevel(logging.DEBUG)
468
+ logging.basicConfig() # Send log records to the console
454
469
 
455
470
 
456
471
  async def main():
457
472
  async with VeSync("user", "password") as manager:
458
- manager.debug = True
459
473
  manager.redact = True # True by default
474
+ LibraryLogger.verbose = True # Optional - print full request/response content
460
475
  await manager.login()
461
476
  await manager.update()
462
477
 
463
- outlet = manager.outlets[0]
478
+ outlet = manager.devices.outlets[0]
464
479
  await outlet.update()
465
480
  await outlet.turn_off()
466
481
  outlet.display()
@@ -470,40 +485,58 @@ if __name__ == "__main__":
470
485
  asyncio.run(main())
471
486
  ```
472
487
 
473
- ### Logging to File
488
+ ### Debug to a file (recommended for sharing with issues)
474
489
 
475
- To log to a file, use the `log_to_file()` method of the `VeSync` class. Pass the file path as an argument.
490
+ The `log_to_file()` method is the simplest way to capture a full debug log. It sets the `pyvesync` logger to `DEBUG` **and** attaches a file handler in one call, so you do not need to configure logging yourself. It also prints to the console by default; pass `stdout=False` to log only to the file. Keep `redact=True` (the default) so the log is safe to attach to a GitHub issue:
476
491
 
477
492
  ```python
478
-
479
493
  import asyncio
480
494
  from pyvesync import VeSync
495
+ from pyvesync.utils.logs import LibraryLogger
496
+
481
497
 
482
498
  async def main():
483
- async with VeSync("user", "password") as manager:
484
- manager.log_to_file("pyvesync.log", level=logging.DEBUG, stdout=True) # stdout argument prints log to console as well
499
+ # redact=True is the default - sensitive values are scrubbed from the log
500
+ async with VeSync("user", "password", redact=True) as manager:
501
+ # Enables DEBUG logging and writes it to pyvesync.log
502
+ # stdout=True (the default) also prints the log to the console
503
+ manager.log_to_file("pyvesync.log", stdout=True)
504
+
505
+ # Optional - include the full request & response body of every API call
506
+ LibraryLogger.verbose = True
507
+
485
508
  await manager.login()
486
509
  await manager.update()
487
510
 
488
- outlet = manager.outlets[0]
511
+ outlet = manager.devices.outlets[0]
489
512
  await outlet.update()
490
513
  await outlet.turn_off()
491
514
  outlet.display()
492
515
 
516
+
493
517
  if __name__ == "__main__":
494
518
  asyncio.run(main())
495
-
496
519
  ```
497
520
 
521
+ ## Contribution Expectations
522
+
523
+ Contributions — from humans and AI coding assistants alike — must follow three rules. These keep the library correct and maintainable. See the [contributing guide](https://webdjoe.github.io/pyvesync/latest/development/contributing) for full detail.
524
+
525
+ 1. **Follow the intended architecture.** New devices are added through the three-tier hierarchy (base class → concrete device + API mixin → device map), with state in the device's `DeviceState` subclass, models in `models/`, and constants in `pyvesync.const`. API request/response shapes must come from a real packet capture — never invented.
526
+ 2. **Verify on real hardware before merge.** New device support and any change to device behavior must be tested on the physical device, and the pull request must state the model and firmware version tested. Testing only through Home Assistant does not count.
527
+ 3. **One change per pull request.** Each PR must be a single feature, fix, or edit. Large or mixed PRs that bundle unrelated changes will be asked to be broken up before review.
528
+
529
+ > **Using an AI assistant?** Agent instructions live in [`AGENTS.md`](AGENTS.md), which Codex and most tools read directly; [`CLAUDE.md`](CLAUDE.md) imports it for Claude Code. Point your assistant at it and the [contributing guide](https://webdjoe.github.io/pyvesync/latest/development/contributing). You are responsible for the result: it must follow the architecture, must not fabricate API fields, must not be submitted without real-device testing, and must be a single focused change.
530
+
498
531
  ## Feature Requests
499
532
 
500
- Before filing an issue to request a new feature or device, please ensure that you will take the time to test the feature throuroughly. New features cannot be simply tested on Home Assistant. A separate integration must be created which is not part of this library. In order to test a new feature, clone the branch and install into a new virtual environment.
533
+ Before filing an issue to request a new feature or device, please ensure that you will take the time to test the feature thoroughly. New features cannot be simply tested on Home Assistant. A separate integration must be created which is not part of this library. In order to test a new feature, clone the branch and install into a new virtual environment.
501
534
 
502
535
  ```bash
503
536
  mkdir python_test && cd python_test
504
537
 
505
538
  # Check Python version is 3.11 or higher
506
- python3 --version # or python --version or python3.8 --version
539
+ python3 --version # or python --version
507
540
  # Create a new venv
508
541
  python3 -m venv pyvesync-venv
509
542
  # Activate the venv on linux
@@ -521,14 +554,11 @@ pip install git+https://github.com/webdjoe/pyvesync.git@refs/pull/PR_NUMBER/head
521
554
 
522
555
  Test functionality with a script, please adjust methods and logging statements to the device you are testing.
523
556
 
524
- See `[testing_scripts](./testing_scripts/README.md)` for a fully functioning test script for all devices.
557
+ See [testing_scripts](./testing_scripts/README.md) for a fully functioning test script for all devices.
525
558
 
526
559
  ```python
527
560
  import asyncio
528
- import sys
529
561
  import logging
530
- import json
531
- from functool import chain
532
562
  from pyvesync import VeSync
533
563
 
534
564
  vs_logger = logging.getLogger("pyvesync")
@@ -540,65 +570,47 @@ logger.setLevel(logging.DEBUG)
540
570
  USERNAME = "YOUR USERNAME"
541
571
  PASSWORD = "YOUR PASSWORD"
542
572
 
543
- DEVICE_NAME = "Device" # Device to test
573
+ DEVICE_NAME = "Device" # Device to test
544
574
 
545
575
  async def test_device():
546
576
  # Instantiate VeSync class and login
547
- async with VeSync(USERNAME, PASSWORD, redact=True) as manager:
548
- await manager.login()
549
-
550
- # Pull and update devices
551
- await manager.update()
552
-
553
- for dev in manager.devices:
554
- # Print all device info
555
- logger.debug(dev.device_name + "\n")
556
- logger.debug(dev.display())
557
-
558
- # Find correct device
559
- if dev.device_name.lower() != DEVICE_NAME.lower():
560
- logger.debug("%s is not %s, continuing", self.device_name, DEVICE_NAME)
561
- continue
562
-
563
- logger.debug('--------------%s-----------------' % dev.device_name)
564
- logger.debug(dev.display())
565
- logger.debug(dev.displayJSON())
566
- # Test all device methods and functionality
567
- # Test Properties
568
- logger.debug("Fan is on - %s", dev.is_on)
569
- logger.debug("Modes - %s", dev.modes)
570
- logger.debug("Fan Level - %s", dev.fan_level)
571
- logger.debug("Fan Air Quality - %s", dev.air_quality)
572
- logger.debug("Screen Status - %s", dev.screen_status)
573
-
574
- logger.debug("Turning on")
575
- await fan.turn_on()
576
- logger.debug("Device is on %s", dev.is_on)
577
-
578
- logger.debug("Turning off")
579
- await fan.turn_off()
580
- logger.debug("Device is on %s", dev.is_on)
581
-
582
- logger.debug("Sleep mode")
583
- fan.sleep_mode()
584
- logger.debug("Current mode - %s", dev.details['mode'])
585
-
586
- fan.auto_mode()
587
-
588
- logger.debug("Set Fan Speed - %s", dev.set_fan_speed)
589
- logger.debug("Current Fan Level - %s", dev.fan_level)
590
- logger.debug("Current mode - %s", dev.mode)
591
-
592
- # Display all device info
593
- logger.debug(dev.display(state=True))
594
- logger.debug(dev.to_json(state=True, indent=True))
595
- dev_dict = dev.to_dict(state=True)
577
+ async with VeSync(USERNAME, PASSWORD, redact=True) as manager:
578
+ await manager.login()
579
+
580
+ # Pull and update devices
581
+ await manager.update()
582
+
583
+ for dev in manager.devices:
584
+ # Print all device info
585
+ logger.debug(dev.device_name)
586
+ dev.display()
587
+
588
+ # Find correct device
589
+ if dev.device_name.lower() != DEVICE_NAME.lower():
590
+ logger.debug("%s is not %s, continuing", dev.device_name, DEVICE_NAME)
591
+ continue
592
+
593
+ logger.debug('-------------- %s -----------------', dev.device_name)
594
+ dev.display()
595
+
596
+ # Test device methods
597
+ logger.debug("Device is on: %s", dev.is_on)
598
+
599
+ logger.debug("Turning on")
600
+ await dev.turn_on()
601
+ logger.debug("Device is on: %s", dev.is_on)
602
+
603
+ logger.debug("Turning off")
604
+ await dev.turn_off()
605
+ logger.debug("Device is on: %s", dev.is_on)
606
+
607
+ # Display all device info as JSON
608
+ logger.debug(dev.to_json(state=True, indent=True))
609
+ dev_dict = dev.to_dict(state=True)
596
610
 
597
611
  if __name__ == "__main__":
598
612
  logger.debug("Testing device")
599
613
  asyncio.run(test_device())
600
- ...
601
-
602
614
  ```
603
615
 
604
616
  ## Device Requests