From eb64979e0131fef9ae7f9e07238adb3f2d126674 Mon Sep 17 00:00:00 2001 From: Joe Trabulsy Date: Wed, 15 Apr 2026 21:41:57 -0400 Subject: [PATCH 1/2] Docs updates and fixes --- README.md | 244 ++++++++++++------------- docs/authentication.md | 14 +- docs/development/capturing.md | 2 +- docs/development/contributing.md | 290 +++++++++++++++++++++++++++--- docs/development/data_models.md | 2 +- docs/development/index.md | 26 +-- docs/development/testing.md | 98 ++++++++++ docs/development/utils/logging.md | 52 ++++++ docs/devices/humidifiers.md | 6 +- docs/index.md | 63 +++---- docs/pyvesync3.md | 19 +- docs/supported_devices.md | 22 ++- docs/usage.md | 8 +- 13 files changed, 618 insertions(+), 228 deletions(-) diff --git a/README.md b/README.md index 52fed4a7..9668a94b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 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/) +# 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/) pyvesync is a library to manage VeSync compatible [smart home devices](#supported-devices) @@ -35,7 +35,6 @@ Some of the changes are: - Implemented custom exceptions and error (code) handling for API responses. - `const` module to hold all library constants - Built the `DeviceMap` class to hold the mapping and features of devices. -- COMING SOON: Use API to pull device modes and operating features. See [pyvesync V3](https://webdjoe.github.io/pyvesync/latest/pyvesync3/) for more information on the changes. @@ -46,7 +45,7 @@ Library is now asynchronous, using aiohttp as a replacement for requests. The `p ```python import asyncio import aiohttp -from pyvesync.vesync import VeSync +from pyvesync import VeSync async def main(): async with VeSync( @@ -71,7 +70,7 @@ async def main(): if not manager.enabled: print("Not logged in.") return - await manager.get_devices() # Instantiates supported devices in device list, automatically called by login, only needed if you would like updates + await manager.get_devices() # Instantiates supported devices in device list await manager.update() # Updates the state of all devices # manager.devices is a DeviceContainer object @@ -92,8 +91,8 @@ async def main(): # State of object held in `device.state` attribute print(outlet.state) - state_json = outlet.dumps() # Returns JSON string of device state - state_bytes = orjson.dumps(outlet.state) # Returns bytes of device state + state_json = outlet.to_json(state=True) # Returns JSON string of device state + state_bytes = outlet.to_jsonb(state=True) # Returns JSON bytes of device state # to view the response information of the last API call print(outlet.last_response) @@ -105,7 +104,7 @@ async def main(): session = aiohttp.ClientSession() async def main(): - async with VeSync("user", "password", session=session): + async with VeSync("user", "password", session=session) as manager: await manager.login() await manager.update() @@ -118,7 +117,7 @@ if __name__ == "__main__": If using `async with` is not ideal, the `__aenter__()` and `__aexit__()` methods need to be called manually: ```python -manager = VeSync(user, password) +manager = VeSync("user", "password") await manager.__aenter__() @@ -127,7 +126,7 @@ await manager.__aenter__() await manager.__aexit__(None, None, None) ``` -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: +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: ```text 2025-02-16 14:41:07 - ERROR - asyncio - Unclosed client session @@ -142,9 +141,9 @@ The VeSync signature is: VeSync( username: str, password: str, - country_code: str = DEFAULT_COUNTRY_CODE, # US + country_code: str = DEFAULT_REGION, # "US" session: ClientSession | None = None, - time_zone: str = DEFAULT_TZ # America/New_York, + time_zone: str = DEFAULT_TZ, # "America/New_York" redact: bool = True, ) ``` @@ -161,6 +160,7 @@ There is a new nomenclature for product types that defines the device class. The 5. `humidifier` - Humidifiers (not air purifiers) 6. `bulb` - Light bulbs (not dimmers or switches) 7. `airfryer` - Air fryers +8. `thermostat` - Thermostats See [Supported Devices](#supported-devices) for a complete list of supported devices and models. @@ -170,22 +170,22 @@ Exceptions are no longer caught by the library and must be handled by the user. 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. -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. +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. Custom Exceptions raised by all API calls: -- `pyvesync.logs.VeSyncServerError` - The API connected and returned a code indicated there is a server-side error. -- `pyvesync.logs.VeSyncRateLimitError` - The API's rate limit has been exceeded. -- `pyvesync.logs.VeSyncAPIStatusCodeError` - The API returned a non-200 status code. -- `pyvesync.logs.VeSyncAPIResponseError` - The response from the API was not in an expected format. +- `pyvesync.utils.errors.VeSyncServerError` - The API connected and returned a code indicated there is a server-side error. +- `pyvesync.utils.errors.VeSyncRateLimitError` - The API's rate limit has been exceeded. +- `pyvesync.utils.errors.VeSyncAPIStatusCodeError` - The API returned a non-200 status code. +- `pyvesync.utils.errors.VeSyncAPIResponseError` - The response from the API was not in an expected format. Login API Exceptions -- `pyvesync.logs.VeSyncLoginError` - The username or password is incorrect. +- `pyvesync.utils.errors.VeSyncLoginError` - The username or password is incorrect. -See [errors](https://webdjoe.github.io/pyvesync/latest/development/utils/errors) documentation for a complete list of error codes and exceptions. +See the [errors](https://webdjoe.github.io/pyvesync/latest/development/utils/errors/) documentation for a complete list of error codes and exceptions. -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: +The `raise_api_errors()` function is called for every API call and checks for general response errors. It can raise the following exceptions: - `VeSyncServerError` - The API connected and returned a code indicated there is a server-side error. - `VeSyncRateLimitError` - The API's rate limit has been exceeded. @@ -209,12 +209,14 @@ pip install pyvesync ### Etekcity Outlets -1. Voltson Smart WiFi Outlet- Round (7A model ESW01-USA) +1. Voltson Smart WiFi Outlet - Round (7A model ESW01-USA) 2. Voltson Smart WiFi Outlet - Round (10A model ESW01-EU) -3. Voltson Smart Wifi Outlet - Round (10A model ESW03-USA) -4. Voltson Smart Wifi Outlet - Round (10A model ESW10-USA) +3. Voltson Smart WiFi Outlet - Round (10A model ESW03-USA) +4. Voltson Smart WiFi Outlet - Round (10A model ESW10-USA) 5. Voltson Smart WiFi Outlet - Rectangle (15A model ESW15-USA) 6. Two Plug Outdoor Outlet (ESO15-TB) (Each plug is a separate `VeSyncOutlet` object, energy readings are for both plugs combined) +7. BSDOG / Greensun Smart Outlet Series (BSDOG01, BSDOG02, WYSMTOD16A, WM-PLUG and more) +8. WHOPLUG / Greensun Smart Outlet @@ -223,46 +225,54 @@ pip install pyvesync ### Wall Switches 1. Etekcity Smart WiFi Light Switch (model ESWL01) -2. Etekcity Wifi Dimmer Switch (ESD16) +2. Etekcity WiFi 3-Way Switch (model ESWL03) +3. Etekcity WiFi Dimmer Switch (model ESWD16) ### Levoit Air Purifiers 1. LV-PUR131S -2. Core 200S -3. Core 300S -4. Core 400S -5. Core 600S -6. Vital 100S -7. Vital 200S +2. LV-RH131S +3. Core 200S +4. Core 300S +5. Core 400S +6. Core 600S +7. Vital 100S / 200S 8. Everest Air +9. Sprout Air Purifier -### Etekcity Bulbs +### Etekcity / Valceno Bulbs 1. Soft White Dimmable Smart Bulb (ESL100) 2. Cool to Soft White Tunable Dimmable Bulb (ESL100CW) - -### Valceno Bulbs - -1. Valceno Multicolor Bulb (XYD0001) +3. Multicolor Dimmable Bulb (ESL100MC) +4. Valceno Multicolor Bulb (XYD0001) ### Levoit Humidifiers -1. Dual 200S -2. Classic 300S -3. LV600S -4. OasisMist 450S -5. OasisMist 600S +1. Classic 200S +2. Dual 200S +3. Classic 300S +4. LV600S / LV603S +5. OasisMist 450S / 600S 6. OasisMist 1000S +7. Superior 6000S +8. Sprout Humidifier -### Cosori Air Fryer +### Levoit Fans -1. Cosori 3.7 and 5.8 Quart Air Fryer +1. 42 in. Tower Fan (LTF-F422S Series) +2. Pedestal Fan (LPF-R432S Series) -### Fans +### Cosori Air Fryers -1. 42 in. Tower Fan +1. Cosori 3.7 Quart Air Fryer (CS137-AF) +2. Cosori 5.8 Quart Air Fryer (CS158-AF) + +### Thermostats + +1. Aura Thermostat (LTM-A401S-WUS) @@ -271,10 +281,6 @@ pip install pyvesync ```python import asyncio from pyvesync import VeSync -from pyvesync.logs import VeSyncLoginError - -# VeSync is an asynchronous context manager -# VeSync(username, password, redact=True, session=None) async def main(): async with VeSync("user", "password") as manager: @@ -284,14 +290,14 @@ async def main(): # Acts as a set of device instances device_container = manager.devices - outlets = device_container.outlets # List of outlet instances + outlets = device_container.outlets # List of outlet instances outlet = outlets[0] await outlet.update() await outlet.turn_off() outlet.display() - # Iterate of entire device list - for devices in device_container: + # Iterate over entire device list + for device in device_container: device.display() @@ -299,15 +305,11 @@ if __name__ == "__main__": asyncio.run(main()) ``` -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. +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. ```python import asyncio from pyvesync import VeSync -from pyvesync.logs import VeSyncLoginError - -# VeSync is an asynchronous context manager -# VeSync(username, password, redact=True, session=None) async def main(): async with VeSync("user", "password") as manager: @@ -319,32 +321,37 @@ async def main(): await manager.load_credentials_from_file("/path/to/token_file") # Or credentials can be passed directly - manager.set_credentials("your_token", "your_account_id") - - # No login needed + manager.set_credentials( + token="your_token", + account_id="your_account_id", + country_code="US", + region="US", + ) + + # No login needed if token is valid # await manager.login() # To store credentials to a file after login - await manager.save_credentials() # Saves to default location ~/.vesync_token - # or pass a file path await manager.save_credentials("/path/to/token_file") - # Output Credentials as JSON String - await manager.output_credentials() + # Output credentials as JSON string + print(manager.output_credentials_json()) + # Output credentials as dictionary + print(manager.output_credentials_dict()) await manager.update() # Acts as a set of device instances device_container = manager.devices - outlets = device_container.outlets # List of outlet instances + outlets = device_container.outlets # List of outlet instances outlet = outlets[0] await outlet.update() await outlet.turn_off() outlet.display() - # Iterate of entire device list - for devices in device_container: + # Iterate over entire device list + for device in device_container: device.display() @@ -369,7 +376,7 @@ manager.devices.bulbs = [VeSyncBulbInstances] manager.devices.air_purifiers = [VeSyncPurifierInstances] manager.devices.humidifiers = [VeSyncHumidifierInstances] manager.devices.air_fryers = [VeSyncAirFryerInstances] -managers.devices.thermostats = [VeSyncThermostatInstances] +manager.devices.thermostats = [VeSyncThermostatInstances] # Get device by device name dev_name = "My Device" @@ -398,8 +405,7 @@ This is an example of debug mode with redact enabled: ```python import logging import asyncio -import aiohttp -from pyvesync.vesync import VeSync +from pyvesync import VeSync logger = logging.getLogger("pyvesync") logger.setLevel(logging.DEBUG) @@ -412,7 +418,7 @@ async def main(): await manager.login() await manager.update() - outlet = manager.outlets[0] + outlet = manager.devices.outlets[0] await outlet.update() await outlet.turn_off() outlet.display() @@ -427,17 +433,16 @@ if __name__ == "__main__": To log to a file, use the `log_to_file()` method of the `VeSync` class. Pass the file path as an argument. ```python - import asyncio from pyvesync import VeSync async def main(): async with VeSync("user", "password") as manager: - manager.log_to_file("pyvesync.log", level=logging.DEBUG, stdout=True) # stdout argument prints log to console as well + manager.log_to_file("pyvesync.log", stdout=True) # stdout argument prints log to console as well await manager.login() await manager.update() - outlet = manager.outlets[0] + outlet = manager.devices.outlets[0] await outlet.update() await outlet.turn_off() outlet.display() @@ -449,13 +454,13 @@ if __name__ == "__main__": ## Feature Requests -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. +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. ```bash mkdir python_test && cd python_test # Check Python version is 3.11 or higher -python3 --version # or python --version or python3.8 --version +python3 --version # or python --version # Create a new venv python3 -m venv pyvesync-venv # Activate the venv on linux @@ -473,14 +478,11 @@ pip install git+https://github.com/webdjoe/pyvesync.git@refs/pull/PR_NUMBER/head Test functionality with a script, please adjust methods and logging statements to the device you are testing. -See `[testing_scripts](./testing_scripts/README.md)` for a fully functioning test script for all devices. +See [testing_scripts](./testing_scripts/README.md) for a fully functioning test script for all devices. ```python import asyncio -import sys import logging -import json -from functool import chain from pyvesync import VeSync vs_logger = logging.getLogger("pyvesync") @@ -492,65 +494,47 @@ logger.setLevel(logging.DEBUG) USERNAME = "YOUR USERNAME" PASSWORD = "YOUR PASSWORD" -DEVICE_NAME = "Device" # Device to test +DEVICE_NAME = "Device" # Device to test async def test_device(): # Instantiate VeSync class and login - async with VeSync(USERNAME, PASSWORD, redact=True) as manager: - await manager.login() - - # Pull and update devices - await manager.update() - - for dev in manager.devices: - # Print all device info - logger.debug(dev.device_name + "\n") - logger.debug(dev.display()) - - # Find correct device - if dev.device_name.lower() != DEVICE_NAME.lower(): - logger.debug("%s is not %s, continuing", self.device_name, DEVICE_NAME) - continue - - logger.debug('--------------%s-----------------' % dev.device_name) - logger.debug(dev.display()) - logger.debug(dev.displayJSON()) - # Test all device methods and functionality - # Test Properties - logger.debug("Fan is on - %s", dev.is_on) - logger.debug("Modes - %s", dev.modes) - logger.debug("Fan Level - %s", dev.fan_level) - logger.debug("Fan Air Quality - %s", dev.air_quality) - logger.debug("Screen Status - %s", dev.screen_status) - - logger.debug("Turning on") - await fan.turn_on() - logger.debug("Device is on %s", dev.is_on) - - logger.debug("Turning off") - await fan.turn_off() - logger.debug("Device is on %s", dev.is_on) - - logger.debug("Sleep mode") - fan.sleep_mode() - logger.debug("Current mode - %s", dev.details['mode']) - - fan.auto_mode() - - logger.debug("Set Fan Speed - %s", dev.set_fan_speed) - logger.debug("Current Fan Level - %s", dev.fan_level) - logger.debug("Current mode - %s", dev.mode) - - # Display all device info - logger.debug(dev.display(state=True)) - logger.debug(dev.to_json(state=True, indent=True)) - dev_dict = dev.to_dict(state=True) + async with VeSync(USERNAME, PASSWORD, redact=True) as manager: + await manager.login() + + # Pull and update devices + await manager.update() + + for dev in manager.devices: + # Print all device info + logger.debug(dev.device_name) + dev.display() + + # Find correct device + if dev.device_name.lower() != DEVICE_NAME.lower(): + logger.debug("%s is not %s, continuing", dev.device_name, DEVICE_NAME) + continue + + logger.debug('-------------- %s -----------------', dev.device_name) + dev.display() + + # Test device methods + logger.debug("Device is on: %s", dev.is_on) + + logger.debug("Turning on") + await dev.turn_on() + logger.debug("Device is on: %s", dev.is_on) + + logger.debug("Turning off") + await dev.turn_off() + logger.debug("Device is on: %s", dev.is_on) + + # Display all device info as JSON + logger.debug(dev.to_json(state=True, indent=True)) + dev_dict = dev.to_dict(state=True) if __name__ == "__main__": logger.debug("Testing device") asyncio.run(test_device()) -... - ``` ## Device Requests diff --git a/docs/authentication.md b/docs/authentication.md index 65e26814..49b6dac4 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -13,7 +13,7 @@ import asyncio from pyvesync import VeSync async def main(): - with VeSync(username="example@mail.com", password="password") as manager: + async with VeSync(username="example@mail.com", password="password") as manager: # Login success = await manager.login() if not success: @@ -36,7 +36,7 @@ import asyncio from pyvesync import VeSync async def main(): - with VeSync(username="example@mail.com", password="password") as manager: + async with VeSync(username="example@mail.com", password="password") as manager: # Load credentials from a dictionary credentials = { "token": "your_token_here", @@ -44,7 +44,7 @@ async def main(): "country_code": "US", "region": "US" } - success = await manager.set_credentials(**credentials) + manager.set_credentials(**credentials) # Or load from a file await manager.load_credentials_from_file("path/to/credentials.json") @@ -54,7 +54,7 @@ asyncio.run(main()) ### Credential Storage -Credentials can be saved to a file or output as a json string. If no file path is provided the credentials will be saved to the users home directory as `.vesync_auth`. +Credentials can be saved to a file or output as a json string. If no file path is provided the credentials will be saved to the users home directory as `.vesync_token`. The credentials file is a json file that has the keys `token`, `account_id`, `country_code`, and `region`. @@ -66,12 +66,12 @@ from pyvesync import VeSync async def main(): token_file = Path.home() / ".vesync_token" - with VeSync(username="example@mail.com", password="password") as manager: + async with VeSync(username="example@mail.com", password="password") as manager: # Login and save credentials to file - success = await manager.login(token_file_path=token_file) + success = await manager.login() if success: # Save credentials to file - manager.save_credentials(token_file) + await manager.save_credentials(token_file) # Output credentials as json string print(manager.output_credentials_json()) diff --git a/docs/development/capturing.md b/docs/development/capturing.md index 0ae792fa..b3f7e5b1 100644 --- a/docs/development/capturing.md +++ b/docs/development/capturing.md @@ -4,7 +4,7 @@ This document outlines the steps to capture network packets for adding support f The process outlined below is time consuming and can be difficult. An alternative method is to temporarily share the device. If you would prefer this method, please indicate in an issue or contact the maintainer directly. Sharing a device is done by going to the device settings and selecting "Share Device". Please create a post to notify the maintainers to receive the correct email address to share the device with. -Please do not post a device request without being will to either capture packets or share the device. +Please do not post a device request without being willing to either capture packets or share the device. ## Prerequisites diff --git a/docs/development/contributing.md b/docs/development/contributing.md index b47017a5..f5a3fe9d 100644 --- a/docs/development/contributing.md +++ b/docs/development/contributing.md @@ -2,58 +2,292 @@ Contributions are welcome! Please follow the guidelines below to ensure a quick and smooth review process. -Uses the [pre-commit](https://pre-commit.com/) framework to manage and maintain code quality. This is automatically run on `commit` to check for formatting and linting issues by the `pre-commit.ci` service. Running this manually is not required, but recommended to ensure a clean commit: +## Getting Started + +### Install the Development Environment + +```bash +# Clone the repository +git clone https://github.com/webdjoe/pyvesync.git +cd pyvesync + +# Create a virtual environment (Python 3.11+) +python -m venv venv +source venv/bin/activate # Linux/macOS +# .\venv\Scripts\activate.ps1 # Windows PowerShell + +# Install with dev dependencies +pip install -e .[dev] +``` + +### Pre-commit Hooks + +The project uses [pre-commit](https://pre-commit.com/) to enforce code quality on every commit. The [pre-commit.ci](https://pre-commit.ci/) service also runs these checks automatically on pull requests. + +The hooks include: + +- **check-yaml** / **check-toml** / **check-ast** - Validates file syntax +- **trailing-whitespace** / **end-of-file-fixer** - Whitespace cleanup +- **mypy** - Static type checking +- **ruff-check** - Linting with auto-fix +- **ruff-format** - Code formatting + +To install and run pre-commit locally: ```bash - pre-commit run +pre-commit install # Install hooks (runs on every git commit) +pre-commit run # Run on staged files only +pre-commit run --all-files # Run on all files +``` + +/// note +Changes must be staged (`git add`) before running `pre-commit run` for it to check the correct files. +/// + +## Pull Request Process + +### Semantic PR Titles + +Pull request titles must follow the [Conventional Commits](https://www.conventionalcommits.org/) format. This is enforced by a GitHub Action on all PRs. Valid prefixes: + +- `feat:` - New feature or device support +- `fix:` - Bug fix +- `docs:` - Documentation changes +- `refactor:` - Code refactoring (no functional change) +- `test:` - Adding or updating tests +- `chore:` - Maintenance, dependency updates, CI changes + +Examples: + +```text +feat: Add support for LAP-C601S air purifier +fix: Handle token expiration during device update +docs: Update contributing guidelines ``` -**NOTE:** Changes must be staged in order for this to work properly. +### What Happens on a PR + +When you open a pull request targeting `master` or `dev`, the **Run Linting and Unit Tests** workflow runs automatically: + +1. **Ruff** - Lints the codebase with `ruff check --output-format=github` +2. **Pylint** - Runs pylint on `src/pyvesync` +3. **Pytest** - Runs the full test suite across Python 3.11, 3.12, and 3.13 +4. **MkDocs Build** - Builds the documentation (only on PRs to `master`, Python 3.12) + +All four checks must pass for the PR to be merged. ## Code Style +### Ruff Configuration + +The project uses [ruff](https://docs.astral.sh/ruff/) as the primary linter and formatter. The configuration is in `ruff.toml` with the following key settings: + +- **Line length**: 90 characters +- **Indent**: 4 spaces +- **Rule selection**: `ALL` (all rules enabled, with specific ignores) +- **Docstring convention**: Google style +- **Quote style**: Single quotes (double quotes for docstrings) + +Ruff runs with auto-fix enabled in pre-commit, so many issues are corrected automatically on commit. + ### General Style Guidelines -- Single quotes for strings, except when the string contains a single quote or is a docstring. -- Use f-strings for string formatting. -- Use type hinting for function signatures and variable declarations. -- Docstrings for all public classes, methods and functions. Not required for inherited methods and properties. -- Constants should be stored in the `const` module, there should be no hardcoded strings or numbers in the code. -- Line length is limited to 90 characters. -- Classes and public variables must be camel cased. -- Local variables, methods and properties must be snake cased. -- Imports must be sorted and grouped by standard library, third party and local imports. +- **Quotes**: Single quotes for all strings. Double quotes for docstrings. + + ```python + name = 'my_device' # Single quotes + msg = "it's a device" # Double quotes when string contains single quote + + def update(self): + """Update device state.""" # Double quotes for docstrings + ``` + +- **String formatting**: Use f-strings. + + ```python + logger.debug('Device %s status: %s', self.device_name, status) # logging uses %s + message = f'Device {self.device_name} updated' # f-strings elsewhere + ``` + +- **Type hints**: Required for all function signatures. Use `|` union syntax (Python 3.10+), not `Union` or `Optional`. Use `from __future__ import annotations` at the top of every module. + + ```python + from __future__ import annotations + + def set_brightness(self, brightness: int) -> bool: ... + def get_config(self) -> OutletMap | None: ... + async def call_api(self, data: dict | None = None) -> dict | None: ... + ``` + +- **TYPE_CHECKING imports**: Imports used only for type hints should be guarded behind `if TYPE_CHECKING:` to avoid circular imports at runtime. + + ```python + from __future__ import annotations + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from pyvesync import VeSync + from pyvesync.device_map import OutletMap + ``` + +- **Import ordering**: Imports are grouped and sorted by: (1) `__future__`, (2) standard library, (3) third-party, (4) local imports. Each group is separated by a blank line. + + ```python + from __future__ import annotations + + import logging + from typing import TYPE_CHECKING + + from mashumaro.mixins.orjson import DataClassORJSONMixin + + from pyvesync.base_devices.outlet_base import VeSyncOutlet + from pyvesync.const import DeviceStatus, ConnectionStatus + ``` + +- **Docstrings**: Required for all public classes, methods, and functions. Use Google-style format. Not required for inherited/overridden methods. + + ```python + class OutletState(DeviceState): + """Base state class for Outlets. + + This class holds all of the state information for the outlet devices. + + Args: + device (VeSyncOutlet): The device object. + details (ResponseDeviceDetailsModel): The device details. + feature_map (OutletMap): The feature map for the device. + + Attributes: + energy (float): Energy usage in kWh. + power (float): Power usage in Watts. + voltage (float): Voltage in Volts. + + Note: + Not all attributes are available on all devices. + """ + ``` + +- **Line length**: 90 characters maximum. + +- **Naming conventions**: + + | Element | Convention | Example | + | ------------------- | ------------------------------- | -------------------------------------- | + | Classes | PascalCase | `VeSyncOutlet7A`, `OutletState` | + | Methods / functions | snake_case | `turn_on()`, `set_brightness()` | + | Properties | snake_case | `device_status`, `fan_level` | + | Constants | UPPER_SNAKE_CASE | `DEFAULT_TZ`, `STATUS_OK` | + | Enums | PascalCase class, UPPER members | `DeviceStatus.ON` | + | Module-level logger | `logger` | `logger = logging.getLogger(__name__)` | + +- **Constants**: All constants, default values, and device modes must be defined in the `pyvesync.const` module. No hardcoded strings or magic numbers in device code. Use `StrEnum` or `IntEnum` for enum values. + + ```python + # In const.py + class DeviceStatus(StrEnum): + ON = 'on' + OFF = 'off' + + # In device code - use the enum, not the raw string + self.state.device_status = DeviceStatus.ON # correct + self.state.device_status = 'on' # incorrect + ``` + +- **`__slots__`**: Used on state classes and the `VeSync` manager class to restrict attribute creation and improve memory usage. ### Device Method and Attribute Naming -- All states specific to a device type must be stored in the `DeviceState` class in the base device type module. For example, `SwitchState` for switches, `PurifierState` for purifiers, etc. +- All states specific to a device type must be stored in the `DeviceState` subclass in the base device type module. For example, `SwitchState` for switches, `PurifierState` for purifiers, etc. - All device properties and methods are to be created in the specific device type base class, not in the implementation device class. -- All device methods that set one or the other binary state must be named `turn__on()` or `turn__off()`. For example, `turn_on()`, `turn_off()`, `turn_child_lock_on()`, `turn_child_lock_off()`. -- The `turn_on()` and `turn_off()` are specific methods that use the `toggle_switch()` method. Any method that toggles a binary state must be named `toggle_()`. For example, `toggle_lock()`, `toggle_mute()`, `toggle_child_lock()`. -- Methods that set a specific state that is not on/off must be named `set_()`. For example, `set_brightness()`, `set_color()`, `set_temperature()`. +- Binary state methods follow this naming pattern: + + | Pattern | Usage | Example | + | ------------------------------------------ | -------------------- | ----------------------------------------------- | + | `turn_on()` / `turn_off()` | Power on/off | Inherited from `VeSyncBaseToggleDevice` | + | `turn__on()` / `turn__off()` | Named binary state | `turn_child_lock_on()`, `turn_child_lock_off()` | + | `toggle_(bool)` | Toggle binary state | `toggle_child_lock()`, `toggle_display()` | + | `set_(value)` | Set non-binary state | `set_brightness()`, `set_fan_level()` | + +- The `turn_on()` and `turn_off()` methods are specific to power and call the `toggle_switch()` method internally. + +### Models Directory + +Data model files in `pyvesync/models/` have relaxed naming rules (`N803`, `N804`, `N802`, `N815` ignored) because model field names must match the VeSync API's JSON keys exactly (e.g., `traceId`, `accountID`, `configModule`). ## Testing and Linting -For convenience, the `tox` can be used to run tests and linting. This requires `tox` to be installed in your Python environment. +### Running Tests Locally -To run all tests and linting: +```bash +# Run all tests +pytest + +# Run a specific test file +pytest src/tests/test_outlets.py + +# Write API fixtures for new devices +pytest --write_api +pytest --write_api --overwrite # Overwrite existing fixtures +``` + +### Running with Tox + +For convenience, `tox` can be used to run tests and linting. This requires `tox` to be installed in your Python environment. ```bash - tox +# Run all environments +tox + +# Specific environments +tox -e 3.11 # Run tests with Python 3.11 +tox -e 3.12 # Run tests with Python 3.12 +tox -e 3.13 # Run tests with Python 3.13 +tox -e lint # Run pylint checks +tox -e flake8 # Run flake8 checks +tox -e ruff # Run ruff checks +tox -e mypy # Run mypy type checks ``` -Specific test environments: +### Running Linters Directly ```bash - tox -e py38 # Run tests with Python 3.8 - tox -e py39 # Run tests with Python 3.9 - tox -e py310 # Run tests with Python 3.10 - tox -e py311 # Run tests with Python 3.11 - tox -e lint # Run pylint checks - tox -e ruff # Run ruff checks - tox -e mypy # Run mypy type checks +ruff check src/pyvesync # Lint +ruff format src/pyvesync # Format +mypy src/pyvesync # Type check +pylint src/pyvesync # Pylint ``` +See the [Testing](./testing.md) documentation for details on the test architecture, fixtures, and adding tests for new devices. + +## Release Process + +Releases are triggered automatically when code is merged to `master`. The **Release and Publish** workflow: + +1. **Extracts the version** from `pyproject.toml` +2. **Validates** the new version is greater than the latest git tag +3. **Builds** the distribution package (`python -m build`) +4. **Creates a GitHub Release** with auto-generated release notes and the version as the tag (e.g., `3.4.1`) +5. **Publishes to PyPI** via the `pypa/gh-action-pypi-publish` action +6. **Deploys documentation** using `mike` to GitHub Pages, updating the `latest` alias + +### Versioning + +The project version is defined in `pyproject.toml` under `[project].version`. The version follows [semantic versioning](https://semver.org/): + +- **Major** (x.0.0) - Breaking changes +- **Minor** (0.x.0) - New features, new device support +- **Patch** (0.0.x) - Bug fixes + +When preparing a release PR to `master`, bump the version in `pyproject.toml`. The release workflow will fail if the new version is not greater than the previous tag. + +### Documentation Deployment + +Documentation is built with MkDocs and deployed to GitHub Pages using [mike](https://github.com/jimporter/mike) for version management. Each release creates a versioned deployment and updates the `latest` alias. The documentation site is at [pyvesync.github.io](https://pyvesync.github.io/). + +## Dependency Management + +[Dependabot](https://docs.github.com/en/code-security/dependabot) is configured to check for updates weekly to both pip dependencies and GitHub Actions versions. + ## Requests to Add Devices -Please see [CAPTURING.md](./capturing.md) for instructions on how to capture the necessary information to add a new device. +Please see [Capturing](./capturing.md) for instructions on how to capture the necessary information to add a new device. diff --git a/docs/development/data_models.md b/docs/development/data_models.md index 76e86fe2..6fd48cfa 100644 --- a/docs/development/data_models.md +++ b/docs/development/data_models.md @@ -3,7 +3,7 @@ Data models are used to strongly type and verify API request and response structure. All API calls require a data model to be passed in as a parameter. Each module in the `pyvesync.models` module is for a specific product type or API call. -The dataclasses inherit from mashumaro's `DataClassORJSONMixin` which allows for easy serialization and deserialization of the data models, as well as providing a discrimintor for subclasses. +The dataclasses inherit from mashumaro's `DataClassORJSONMixin` which allows for easy serialization and deserialization of the data models, as well as providing a discriminator for subclasses. The `bypassv2_models` module is a generic mixin for the bypassv2 API calls. diff --git a/docs/development/index.md b/docs/development/index.md index 7c4173e5..e37e27ef 100644 --- a/docs/development/index.md +++ b/docs/development/index.md @@ -1,6 +1,6 @@ # pyvesync Library Development -This is a community driven library, so contributions are welcome! Due to the size of the library and variety of API calls and devices there are guidelines that need to be followed to ensure the continued development and maintanability. +This is a community driven library, so contributions are welcome! Due to the size of the library and variety of API calls and devices there are guidelines that need to be followed to ensure the continued development and maintainability. There is a new nomenclature for product types that defines the device class. The `device.product_type` attribute defines the product type based on the VeSync API. The product type is used to determine the device class and module. The currently supported product types are: @@ -12,12 +12,13 @@ There is a new nomenclature for product types that defines the device class. The 5. `humidifier` - Humidifiers (not air purifiers) 6. `bulb` - Light bulbs (not dimmers or switches) 7. `airfryer` - Air fryers +8. `thermostat` - Thermostats ## Architecture The `pyvesync.vesync.VeSync` class, also referred to as the `manager` is the central control for the entire library. This is the only class that should be directly instantiated. -The `VeSync` instance contains the authentication information and holds the device objects. The `VeSync` class has the method `async_call_api` which should be used for all API calls. It is as you might has guessed asynchronous. The session can either be passed in when instantiating the manager or generated internally. +The `VeSync` instance contains the authentication information and holds the device objects. The `VeSync` class has the method `async_call_api` which should be used for all API calls. It is as you might have guessed asynchronous. The session can either be passed in when instantiating the manager or generated internally. Devices have a base class in the `pyvesync.base_devices` module. Each device type has a separate module that contains the device class and the API calls that are specific to that device type. The device classes inherit from the `VeSyncBaseDevice` and `VeSyncToggleDevice` base classes and implement the API calls for that device type. @@ -135,7 +136,7 @@ class ResponseCodeModel(ResponseBaseModel): code: int msg: str | None -```` +``` Models for each device should be kept in the `data_models` folder with the appropriate device name: @@ -145,6 +146,9 @@ Models for each device should be kept in the `data_models` folder with the appro - `outlet_models` - `switch_models` - `fan_models` +- `fryer_models` +- `thermostat_models` +- `bypass_models` There are multiple levels to some requests with nested dictionaries. These must be defined in different classes: @@ -271,7 +275,7 @@ assert api_bool == True ## Device Map -All features and configuration options for devices are held in the `pyveysnc.device_map` module. Older versions of pyvesync held the device configuration in each device module, all of these have moved to the `device_map` module. Each product type has a dataclass structure that is used to define all of the configuration options for each type. The `device_map.get_device_config(device_type: str)` method is used to lookup the configuration dataclass instance by the `deviceType` value in the device list response. +All features and configuration options for devices are held in the `pyvesync.device_map` module. Older versions of pyvesync held the device configuration in each device module, all of these have moved to the `device_map` module. Each product type has a dataclass structure that is used to define all of the configuration options for each type. The `device_map.get_device_config(device_type: str)` method is used to lookup the configuration dataclass instance by the `deviceType` value in the device list response. There are also methods for each device to return the device configuration with the correct type. For example, `get_outlet_config()` returns the configuration for the outlet device. The configuration is a dataclass that contains all of the attributes for that device type. The configuration is used to define the attributes in the device state class. @@ -293,18 +297,18 @@ Exceptions are no longer caught by the library and must be handled by the user. 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. -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. +When the connection to the VeSync API succeeds but returns an error code that prevents the library from functioning a custom exception inherited from [`VeSyncError`][pyvesync.utils.errors.VeSyncError] is raised. Custom Exceptions raised by all API calls: -- `pyvesync.logs.VeSyncServerError` - The API connected and returned a code indicated there is a server-side error. -- `pyvesync.logs.VeSyncRateLimitError` - The API's rate limit has been exceeded. -- `pyvesync.logs.VeSyncAPIStatusCodeError` - The API returned a non-200 status code. -- `pyvesync.logs.VeSyncAPIResponseError` - The response from the API was not in an expected format. +- [`VeSyncServerError`][pyvesync.utils.errors.VeSyncServerError] - The API connected and returned a code indicated there is a server-side error. +- [`VeSyncRateLimitError`][pyvesync.utils.errors.VeSyncRateLimitError] - The API's rate limit has been exceeded. +- [`VeSyncAPIStatusCodeError`][pyvesync.utils.errors.VeSyncAPIStatusCodeError] - The API returned a non-200 status code. +- [`VeSyncAPIResponseError`][pyvesync.utils.errors.VeSyncAPIResponseError] - The response from the API was not in an expected format. Login API Exceptions -- `pyvesync.logs.VeSyncLoginError` - The username or password is incorrect. +- [`VeSyncLoginError`][pyvesync.utils.errors.VeSyncLoginError] - The username or password is incorrect. See [errors.py](./utils/errors.md) for a complete list of error codes and exceptions. @@ -540,7 +544,7 @@ The response structure has a relatively similar structure for all calls with a n #### Bypass V2 Device Mixin -The `pyvesync.utils.device_mixins.BypassV2Mixin` class contains boilerplate code for the devices that use the Bypass V1 api. The mixin contains the `call_bypassv1_mixin` method that builds the request and calls the api. The method accepts the following parameters: +The `pyvesync.utils.device_mixins.BypassV2Mixin` class contains boilerplate code for the devices that use the Bypass V2 api. The mixin contains the `call_bypassv2_api` method that builds the request and calls the api. The method accepts the following parameters: ```python async def call_bypassv2_api( diff --git a/docs/development/testing.md b/docs/development/testing.md index e69de29b..e2437ee6 100644 --- a/docs/development/testing.md +++ b/docs/development/testing.md @@ -0,0 +1,98 @@ +# Testing + +Tests use pytest with parametrized fixtures. The test suite verifies API requests made by pyvesync devices at two levels. + +## Running Tests + +```bash +# Run all tests +pytest + +# Run a specific test file +pytest src/tests/test_outlets.py + +# Run with tox (all environments) +tox + +# Individual tox environments +tox -e 3.11 # pytest with Python 3.11 +tox -e 3.12 # pytest with Python 3.12 +tox -e 3.13 # pytest with Python 3.13 +tox -e lint # pylint +tox -e flake8 # flake8 + pydocstrings +tox -e mypy # type checking +tox -e ruff # ruff linting +``` + +## Test Architecture + +### TestBase - Device Method Tests + +`TestBase` in `src/tests/base_test_cases.py` patches `VeSync.async_call_api()` to mock all device method calls. This is the primary testing approach for device tests. The mock intercepts the call before any HTTP request is made, capturing the `url`, `method`, `json_object`, and `headers` arguments. + +The test flow: + +1. Set `mock_api.return_value` to the response from the `call_json_*` module +2. Instantiate device via `self.get_device(product_type, setup_entry)` +3. Call the device method (e.g., `outlet_obj.turn_on()`) +4. `parse_args(self.mock_api)` extracts the captured `call_api` arguments +5. `assert_test()` scrubs sensitive data via `api_scrub()`, then compares against the existing YAML fixture + +### TestApiFunc - HTTP-level Tests + +`TestApiFunc` patches `aiohttp.ClientSession` directly, testing the full HTTP request/response cycle. Uses `AiohttpMockSession` from `aiohttp_mocker.py` to simulate aiohttp responses. Used for login tests and API error handling (rate limits, server errors, status codes). + +## Parametrized Test Generation + +Tests are auto-parametrized by `conftest.py:pytest_generate_tests()` based on class attributes: + +```python +class TestOutlets(TestBase): + device = 'outlets' # Device category name + outlets = call_json_outlets.OUTLETS # List of setup_entry strings + base_methods = [['turn_on'], ['turn_off']] # Methods tested on ALL devices + device_methods = { # Methods tested on SPECIFIC devices + 'ESW15-USA': [['turn_on_nightlight'], ['get_weekly_energy']], + } +``` + +This generates two test functions: + +- **`test_details(setup_entry, method)`** - Tests `get_details()` request against YAML fixtures. +- **`test_methods(setup_entry, method)`** - Tests each method's request against YAML fixtures. + +Test IDs follow the pattern: `{device}.{setup_entry}.{method}` (e.g., `outlets.ESW15-USA.turn_on`). + +## YAML API Fixtures + +API requests are recorded and verified via YAML files in `src/tests/api/{module}/{setup_entry}.yaml`. Each YAML file maps method names to the full request captured from the mocked `call_api()`: + +```yaml +turn_off: + headers: { ... } + json_object: { ... } + method: put + url: /outdoorsocket15a/v1/device/devicestatus +``` + +Sensitive values (tokens, account IDs, UUIDs) are normalized to defaults by `api_scrub()` before comparison or writing. + +### Writing API Fixtures + +To generate or update YAML fixtures for new devices: + +```bash +# Write fixtures for new devices (does not overwrite existing) +pytest --write_api + +# Overwrite all existing fixtures (use with caution) +pytest --write_api --overwrite +``` + +## Adding Tests for a New Device + +1. **`call_json_{device_type}.py`**: Add setup_entry to the module's device list. Add response to `DETAILS_RESPONSES[setup_entry]`. Add any non-default method responses to `METHOD_RESPONSES[setup_entry]`. + +2. **`test_{device_type}.py`**: If the device uses existing base/device methods, it is automatically included through parametrization. Add device-specific methods to `device_methods` dict if needed. + +3. **Run `pytest --write_api`** to generate YAML fixtures for new devices. diff --git a/docs/development/utils/logging.md b/docs/development/utils/logging.md index e69de29b..aa874a52 100644 --- a/docs/development/utils/logging.md +++ b/docs/development/utils/logging.md @@ -0,0 +1,52 @@ +# Logging + +The pyvesync library uses Python's standard `logging` module with the logger name `pyvesync`. The `LibraryLogger` class in `pyvesync.utils.logs` provides structured logging with automatic context (module and class name) and helper methods for API call logging. + +::: pyvesync.utils.logs.LibraryLogger + handler: python + options: + show_root_heading: true + group_by_category: true + show_category_heading: true + show_source: false + filters: + - "!^_.*" + merge_init_into_class: true + show_signature_annotations: true + +## Enabling Debug Logging + +Debug logging can be enabled through the `VeSync` manager or directly via Python's logging module: + +```python +import logging +from pyvesync import VeSync + +# Option 1: Set the pyvesync logger level directly +vs_logger = logging.getLogger("pyvesync") +vs_logger.setLevel(logging.DEBUG) + +# Option 2: Use the manager's debug property +async with VeSync(username="EMAIL", password="PASSWORD") as manager: + manager.debug = True +``` + +## Logging to a File + +For verbose debugging, the manager provides a helper to log to a file: + +```python +async with VeSync(username="EMAIL", password="PASSWORD") as manager: + manager.log_to_file("debug.log", stdout=True) + # stdout=True will also print to the console +``` + +## Redacting Sensitive Information + +By default, the library redacts sensitive information (tokens, account IDs) from log output. This can be controlled via the `redact` parameter on the `VeSync` constructor or the `redact` property: + +```python +# Redaction is enabled by default +async with VeSync(username="EMAIL", password="PASSWORD", redact=True) as manager: + manager.redact = False # Disable redaction for debugging +``` diff --git a/docs/devices/humidifiers.md b/docs/devices/humidifiers.md index ea1c5b2b..4af7a9cf 100644 --- a/docs/devices/humidifiers.md +++ b/docs/devices/humidifiers.md @@ -18,7 +18,7 @@ summary: functions: false group_by_category: true - toc_label: "HumidiferState" + toc_label: "HumidifierState" show_root_heading: true show_root_toc_entry: true show_category_heading: true @@ -121,7 +121,7 @@ ::: pyvesync.devices.vesynchumidifier.VeSyncLV600S options: - toc_label: "VeSync Sprout Humidifier" + toc_label: "VeSync LV600S Humidifier" filters: - "!^_.*" summary: @@ -144,7 +144,7 @@ summary: functions: false group_by_category: true - toc_label: "HumidiferState" + toc_label: "VeSyncHumidifier Base" show_root_heading: true show_root_toc_entry: true show_category_heading: true diff --git a/docs/index.md b/docs/index.md index e65535ec..d6bd3360 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,6 +17,7 @@ The following product types are supported: 5. `humidifier` - Humidifiers (not air purifiers) 6. `bulb` - Light bulbs (not dimmers or switches) 7. `airfryer` - Air fryers +8. `thermostat` - Thermostats See [Supported Devices](supported_devices.md) for a complete list of supported devices and models. @@ -82,25 +83,25 @@ async def main(): redact=True # Optional - Redact sensitive information from logs ) as manager: - # VeSync object is now instantiated - await manager.login() - # Check if logged in - assert manager.enabled - # Debug and Redact are optional arguments - manager.redact = True + # VeSync object is now instantiated + await manager.login() + # Check if logged in + assert manager.enabled + # Debug and Redact are optional arguments + manager.redact = True - # Get devices - await manager.get_devices() - # Device objects are now instantiated, but do not have state - await manager.update() # Pulls in state and updates all devices + # Get devices + await manager.get_devices() + # Device objects are now instantiated, but do not have state + await manager.update() # Pulls in state and updates all devices - # Or iterate through devices and update individually - for device in manager.outlets: - await device.update() + # Or iterate through devices and update individually + for device in manager.devices.outlets: + await device.update() - # Or update a product type of devices: - for outlet in manager.devices.outlets: - await outlet.update() + # Or update a product type of devices: + for outlet in manager.devices.outlets: + await outlet.update() if __name__ == "__main__": asyncio.run(main()) @@ -204,7 +205,7 @@ manager.devices.bulbs = [VeSyncBulbInstances] manager.devices.purifiers = [VeSyncPurifierInstances] manager.devices.humidifiers = [VeSyncHumidifierInstances] manager.devices.air_fryers = [VeSyncAirFryerInstances] -managers.devices.thermostats = [VeSyncThermostatInstances] +manager.devices.thermostats = [VeSyncThermostatInstances] ``` ### Debugging and Getting Help @@ -289,7 +290,7 @@ Devices and their attributes and methods can be accessed via the device lists in One device is simple to access, an outlet for example: ```python -for devices in manager.outlets: +for outlet in manager.devices.outlets: print(outlet) print(outlet.device_name) print(outlet.device_type) @@ -371,28 +372,28 @@ Exceptions are no longer caught by the library and must be handled by the user. 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. -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. +When the connection to the VeSync API succeeds but returns an error code that prevents the library from functioning a custom exception inherited from [`VeSyncError`][pyvesync.utils.errors.VeSyncError] is raised. Custom Exceptions raised by all API calls: -- `pyvesync.logs.VeSyncServerError` - The API connected and returned a code indicated there is a server-side error. -- `pyvesync.logs.VeSyncRateLimitError` - The API's rate limit has been exceeded. -- `pyvesync.logs.VeSyncAPIStatusCodeError` - The API returned a non-200 status code. -- `pyvesync.logs.VeSyncAPIResponseError` - The response from the API was not in an expected format. +- [`VeSyncServerError`][pyvesync.utils.errors.VeSyncServerError] - The API connected and returned a code indicated there is a server-side error. +- [`VeSyncRateLimitError`][pyvesync.utils.errors.VeSyncRateLimitError] - The API's rate limit has been exceeded. +- [`VeSyncAPIStatusCodeError`][pyvesync.utils.errors.VeSyncAPIStatusCodeError] - The API returned a non-200 status code. +- [`VeSyncAPIResponseError`][pyvesync.utils.errors.VeSyncAPIResponseError] - The response from the API was not in an expected format. Login API Exceptions -- `pyvesync.logs.VeSyncLoginError` - The username or password is incorrect. +- [`VeSyncLoginError`][pyvesync.utils.errors.VeSyncLoginError] - The username or password is incorrect. -See [errors](https://webdjoe.github.io/pyvesync/latest/development/utils/errors) documentation for a complete list of error codes and exceptions. +See [errors](./development/utils/errors.md) documentation for a complete list of error codes and exceptions. -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: +The [`raise_api_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: -- `VeSyncServerError` - The API connected and returned a code indicated there is a server-side error. -- `VeSyncRateLimitError` - The API's rate limit has been exceeded. -- `VeSyncAPIStatusCodeError` - The API returned a non-200 status code. -- `VeSyncTokenError` - The API returned a token error and requires `login()` to be called again. -- `VeSyncLoginError` - The user name or password is incorrect. +- [`VeSyncServerError`][pyvesync.utils.errors.VeSyncServerError] - The API connected and returned a code indicated there is a server-side error. +- [`VeSyncRateLimitError`][pyvesync.utils.errors.VeSyncRateLimitError] - The API's rate limit has been exceeded. +- [`VeSyncAPIStatusCodeError`][pyvesync.utils.errors.VeSyncAPIStatusCodeError] - The API returned a non-200 status code. +- [`VeSyncTokenError`][pyvesync.utils.errors.VeSyncTokenError] - The API returned a token error and requires `login()` to be called again. +- [`VeSyncLoginError`][pyvesync.utils.errors.VeSyncLoginError] - The user name or password is incorrect. ## Development diff --git a/docs/pyvesync3.md b/docs/pyvesync3.md index 6bb35e37..86b6ad5e 100644 --- a/docs/pyvesync3.md +++ b/docs/pyvesync3.md @@ -13,8 +13,8 @@ Some of the changes are: - Standardized the API for all device to follow a common naming convention. - Custom exceptions and error (code) handling for API responses. - `last_response` attribute on device instances to hold information on the last API response. -- [`DeviceContainer`][pyvesync.device_container] object holds all devices in a mutable set structure with additional convenience methods and properties for managing devices. This is located in the `VeSync.manager.devices` attribute. -- Custom exceptions for better error handling - [`VeSyncError`][pyvesync.utils.errors.VeSyncError], `VeSyncAPIException`, `VeSyncLoginException`, `VeSyncRateLimitException`, `VeSyncNoDevicesException` +- [`DeviceContainer`][pyvesync.device_container] object holds all devices in a mutable set structure with additional convenience methods and properties for managing devices. This is located in the `VeSync.devices` attribute. +- Custom exceptions for better error handling - [`VeSyncError`][pyvesync.utils.errors.VeSyncError], [`VeSyncAPIResponseError`][pyvesync.utils.errors.VeSyncAPIResponseError], [`VeSyncLoginError`][pyvesync.utils.errors.VeSyncLoginError], [`VeSyncRateLimitError`][pyvesync.utils.errors.VeSyncRateLimitError] - Device state has been separated from the device object and is now managed by the device specific subclasses of [`DeviceState`][pyvesync.base_devices.vesyncbasedevice.DeviceState]. The state object is located in the `state` attribute of the device object. - [`const`][pyvesync.const] module to hold all library constants. - [`device_map`][pyvesync.device_map] module holds all device type mappings and configuration. @@ -74,7 +74,7 @@ if __name__ == "__main__": If using `async with` is not ideal, the `__aenter__()` and `__aexit__()` methods need to be called manually: ```python -manager = VeSync(user, password) +manager = VeSync("user", "password") await manager.__aenter__() @@ -83,7 +83,7 @@ await manager.__aenter__() await manager.__aexit__(None, None, None) ``` -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: +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: ```text 2025-02-16 14:41:07 - ERROR - asyncio - Unclosed client session @@ -98,12 +98,14 @@ The VeSync signature is: VeSync( username: str, password: str, + country_code: str = DEFAULT_REGION, # "US" session: ClientSession | None = None, - time_zone: str = DEFAULT_TZ # America/New_York + time_zone: str = DEFAULT_TZ, # "America/New_York" + redact: bool = True, ) ``` -The VeSync class no longer accepts a `debug`. To set debug set the level of the `pyvesync` logger using: +The VeSync class no longer accepts `debug` as a constructor parameter. Instead, `debug` is available as a property on the instantiated object (`manager.debug = True`). Alternatively, set the level of the `pyvesync` logger using: ```python @@ -124,6 +126,7 @@ There is a new nomenclature for product types that defines the device class. The 5. `humidifier` - Humidifiers (not air purifiers) 6. `bulb` - Light bulbs (not dimmers or switches) 7. `airfryer` - Air fryers +8. `thermostat` - Thermostats See [Supported Devices](./supported_devices.md) for a complete list of supported devices and models. @@ -338,7 +341,7 @@ async def main(): for device in manager.devices: print(device) # Prints all devices in the container - manager.update() # Pull state into devices + await manager.update() # Pull state into devices # also holds the product types as properties @@ -362,7 +365,7 @@ The base module should hold all properties and methods that are common to all de ## Device Configuration with device_map module -All features and configuration options for devices are held in the `pyveysnc.device_map` module. Older versions of pyvesync held the device configuration in each device module, all of these have moved to the `device_map` module. Each product type has a dataclass structure that is used to define all of the configuration options for each type. The `device_map.get_device_config(device_type: str)` method is used to lookup the configuration dataclass instance by the `deviceType` value in the device list response. +All features and configuration options for devices are held in the `pyvesync.device_map` module. Older versions of pyvesync held the device configuration in each device module, all of these have moved to the `device_map` module. Each product type has a dataclass structure that is used to define all of the configuration options for each type. The `device_map.get_device_config(device_type: str)` method is used to lookup the configuration dataclass instance by the `deviceType` value in the device list response. ## Constants diff --git a/docs/supported_devices.md b/docs/supported_devices.md index 9dc5af28..1fbd9d0a 100644 --- a/docs/supported_devices.md +++ b/docs/supported_devices.md @@ -10,7 +10,7 @@ The VeSync API supports a variety of devices. The following is a list of devices 2. Outlets - [Etekcity 7A Round Outlet][pyvesync.devices.vesyncoutlet.VeSyncOutlet7A] - [Etekcity 10A Round Outlet EU][pyvesync.devices.vesyncoutlet.VeSyncOutlet10A] - - [Etekcity 10A Rount Outlet USA][pyvesync.devices.vesyncoutlet.VeSyncESW10USA] + - [Etekcity 10A Round Outlet USA][pyvesync.devices.vesyncoutlet.VeSyncESW10USA] - [Etekcity 15A Rectangle Outlet][pyvesync.devices.vesyncoutlet.VeSyncOutlet15A] - [Etekcity 15A Outdoor Dual Outlet][pyvesync.devices.vesyncoutlet.VeSyncOutdoorPlug] - [BSDOG / Greensun Smart Outlet Series][pyvesync.devices.vesyncoutlet.VeSyncBSDOGPlug] - WHOPLUG / GREENSUN @@ -43,7 +43,7 @@ The VeSync API supports a variety of devices. The following is a list of devices - [CS137][pyvesync.devices.vesynckitchen.VeSyncAirFryer158] - 3.7qt Air Fryer - [CS158][pyvesync.devices.vesynckitchen.VeSyncAirFryer158] - 5.8qt Air Fryer 8. Thermostats - - [Aura][pyvesync.devices.vesyncthermostat] Thermostat **Needs testing** + - [Aura][pyvesync.devices.vesyncthermostat] Thermostat ## Device Features @@ -79,6 +79,7 @@ Switches have minimal features, the dimmer switch is the only switch that has ad | Core300s | ✔ | | | | | | Core200s | ✔ | | | | | | LV-PUR131S | ✔ | | | | | +| Sprout Air Purifier | ✔ | | | | | ### Humidifiers @@ -90,11 +91,24 @@ Switches have minimal features, the dimmer switch is the only switch that has ad | LV600S | | ✔ | | OasisMist | | ✔ | | Superior 6000S | | ✔ | +| Sprout Humidifier | | | ### Fans -Tower Fan - Fan Rotate +| Device Name | Oscillation | Multi-Axis Oscillation | +| ------ | ----- | ----- | +| 42" Tower Fan | ✔ | | +| Pedestal Fan | ✔ | ✔ | ### Air Fryers -Air Fryer - All supported features of CS137 and CS158 +| Device Name | Device Type | Temperature Control | Timer | +| ------ | ----- | ----- | ----- | +| Cosori 3.7qt Air Fryer | CS137 | ✔ | ✔ | +| Cosori 5.8qt Air Fryer | CS158 | ✔ | ✔ | + +### Thermostats + +| Device Name | Device Type | Heat | Cool | Auto | Smart Auto | Emergency Heat | +| ------ | ----- | ----- | ----- | ----- | ----- | ----- | +| Aura Thermostat | LTM-A401S-WUS | ✔ | ✔ | ✔ | ✔ | ✔ | diff --git a/docs/usage.md b/docs/usage.md index 3666f6a0..fade0db6 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -24,7 +24,7 @@ The git checkout line is used to switch to the appropriate branch. If you are us cd ~ git clone https://github.com/webdjoe/pyvesync.git cd pyvesync -git checkout dev-2.0 +git checkout dev ``` Then create a new virtual environment and install the library. @@ -57,7 +57,7 @@ python3 -m venv venv source venv/bin/activate # Install branch to be tested into new virtual environment: -pip install git+https://github.com/webdjoe/pyvesync.git@dev-2.0 +pip install git+https://github.com/webdjoe/pyvesync.git@dev # Install a PR that has not been merged using the PR number: pip install git+https://github.com/webdjoe/pyvesync.git@refs/pull/{PR_NUMBER}/head @@ -81,7 +81,7 @@ This method installs the library from source in a folder in the `%USERPROFILE%` cd %USERPROFILE% git clone "https://github.com/webdjoe/pyvesync.git" cd pyvesync -git checkout dev-2.0 +git checkout dev # Check python version is 3.11 or higher python --version # or python3 --version @@ -109,7 +109,7 @@ python -m venv venv # Create a new venv .\venv\Scripts\activate.bat # Activate the venv on cmd.exe # Install branch to be tested into new virtual environment: -pip install git+https://github.com/webdjoe/pyvesync.git@dev-2.0 +pip install git+https://github.com/webdjoe/pyvesync.git@dev # Install a PR that has not been merged using the PR number: pip install git+https://github.com/webdjoe/pyvesync.git@refs/pull/{PR_NUMBER}/head # Or if you are installing the latest release: From b457acd469c5ad2861f6fe29ea5ce5b176a431fb Mon Sep 17 00:00:00 2001 From: Joe Trabulsy Date: Wed, 15 Apr 2026 21:47:26 -0400 Subject: [PATCH 2/2] Bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 035ecdb9..82cf57b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pyvesync" -version = "3.4.1" +version = "3.4.2" description = "pyvesync is a library to manage Etekcity Devices, Cosori Air Fryers, and Levoit Air Purifiers run on the VeSync app." readme = "README.md" requires-python = ">=3.11"