Contributing to pyvesync↲
Contributions are welcome! This is the complete contribution guide. Following it keeps review quick and smooth. A shorter summary lives in the repository's CONTRIBUTING.md.
Contribution Rules↲
These three rules are the foundation of a mergeable contribution. They apply to everyone, and especially to changes generated with the help of an AI coding assistant. They are enforced through review and the pull request checklist, not CI, so please self-attest honestly.
1. Follow the intended architecture↲
pyvesync uses a deliberate three-tier device hierarchy. New work must fit into it rather than working around it:
- Base classes (
src/pyvesync/base_devices/): an abstract base per device type. Device state lives in theDeviceStatesubclass of the relevant base (OutletState,PurifierState, etc.), and device methods/properties belong on the device-type base class, not the concrete implementation class. - Concrete devices (
src/pyvesync/devices/): a specific model, implemented by combining an API mixin (BypassV1MixinorBypassV2Mixin) with the base class. - Device map (
src/pyvesync/device_map.py): maps the API's device-type string to the class, features, and config.
Additional non-negotiables:
- API request/response models go in
src/pyvesync/models/. - Constants, modes, and default values go in
pyvesync.const, with no hardcoded strings or magic numbers in device code. - Never fabricate API request/response fields. They must come from a real packet capture (see Capturing). If no capture exists for the behavior, the work cannot be completed or merged.
2. Verify on real hardware before merge↲
New device support and any change to device behavior must be tested on the physical device before it is merged. This is a hard requirement.
- The pull request must state the model and firmware version it was tested against.
- Testing only through Home Assistant (or another downstream integration) does not satisfy this requirement. You need a script or session that exercises the library directly against the device.
- If you do not own the device, provide a packet capture and/or share the device with a maintainer (see Capturing) so it can be verified before merge.
3. One change per pull request↲
Each pull request must be a single feature, fix, or edit. Split large or mixed changes into separate PRs.
- Do not bundle unrelated changes (e.g. a new device plus a refactor plus a docs overhaul) into one PR.
- Small, focused PRs are reviewable and verifiable. Large speculative PRs that touch many devices at once will be asked to be broken up before review.
Working with AI Coding Assistants↲
AI assistants (Claude Code, Codex, Cursor, Copilot, etc.) are welcome tools, but the human opening the PR is responsible for the result. The repository includes agent instructions that most assistants load automatically:
AGENTS.mdcovers the rules, architecture, commands, and workflow.src/tests/AGENTS.mdcovers the test suite layout, fixtures, and common test tasks.CLAUDE.mdandsrc/tests/CLAUDE.mdimport the AGENTS.md files for Claude Code. Codex and most other tools read AGENTS.md directly. If you edit agent guidance, edit AGENTS.md.
When driving an assistant against this repository:
- Require it to follow the three Contribution Rules above.
- Add new devices by mapping them in
device_map.pyand subclassing the correct base class plus API mixin, not with ad-hoc, one-off code. - Do not let it invent API request/response shapes. If there is no packet capture for a behavior, it cannot be implemented.
- Do not submit device code that has not been run on the physical device.
- Keep each PR to a single change. Do not batch a session's worth of speculative edits into one large PR.
- Provide the diagnostic information described below.
Diagnostic Information to Include↲
Provide the following when requesting a device, proposing a feature, or reporting a bug. The GitHub issue templates (Bug Report and Device Request) prompt for these fields. For packet-capture instructions, see Capturing.
Requesting a new device:
- Device model number and
product_type - Firmware version
- Region / country code
- Link to the product page
- A packet capture, or a stated willingness to share the device with a maintainer
Adding a new feature:
- The target device model(s) the feature was tested on, with firmware version
- The relevant captured API request and response for the new behavior
- A redacted
DEBUGlog of the library exercising the feature
Reporting a bug / troubleshooting:
- pyvesync version and Python version
- Device model and firmware version
- A redacted
DEBUGlog - The
device.last_responseoutput for the failing call - A minimal reproduction script
Capturing a redacted DEBUG log↲
log_to_file() sets the pyvesync logger to DEBUG and attaches a file
handler in one call. Keep redact=True (the default) so tokens, account IDs,
emails and device identifiers are replaced with ##_REDACTED_##:
import asyncio
from pyvesync import VeSync
async def main():
async with VeSync('EMAIL', 'PASSWORD', redact=True) as manager:
manager.log_to_file('pyvesync.log', stdout=True)
await manager.login()
await manager.update()
device = manager.devices.outlets[0]
await device.turn_off()
print(device.last_response) # result of the last API call
asyncio.run(main())
Set LibraryLogger.verbose = True (from pyvesync.utils.logs) to include full
request and response bodies. See Logging for more options.
Review the log before posting it, and never post packet captures publicly.
Getting Started↲
Install the Development Environment↲
# Fork the repository on GitHub, then clone your fork
git clone https://github.com/<your-username>/pyvesync.git
cd pyvesync
git remote add upstream https://github.com/webdjoe/pyvesync.git
# 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 to enforce code quality on
every commit. The pre-commit.ci service also runs these
checks automatically on pull requests. The hooks apply to src/pyvesync only.
The hooks include:
- check-yaml / check-toml / check-ast: validate 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:
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.
Development Workflow↲
-
Branch from
dev.devis the integration branch that all pull requests target.masteris the release branch, and every merge to it publishes a release. -
Make a single, focused change that follows the architecture rules.
- Add or update tests and record API fixtures (see Testing and Linting).
- Run the linters, type checker, and tests locally.
- Update documentation when behavior or the public API changes (see Documentation).
- Open a pull request into
devwith a Conventional Commits title and the pull request template filled in, including the real-hardware verification section.
Note
Do not change the version in pyproject.toml in a feature or fix PR. The
version is bumped in the release PR from dev to master.
Adding a New Device↲
Only start once you have a packet capture covering every request and response the device needs (see Capturing).
- Device map: add a
*Mapentry (OutletMap,PurifierMap, ...) to the matching*_moduleslist indevice_map.py. Setdev_typesto the model strings the API returns, along withclass_name,setup_entry,model_name,model_display, andfeatures(enums frompyvesync.const). Add any type-specific options such as modes or fan levels. - Device class: if an existing class already speaks the device's API,
point the map entry at it. Otherwise add a class in
src/pyvesync/devices/vesync<type>.py, combining the API mixin with the base class (class VeSyncNewPlug(BypassV2Mixin, VeSyncOutlet)), and model it on the closest existing device. - Models: add request/response dataclasses for new payloads in
src/pyvesync/models/<type>_models.py. Field names match the API's JSON keys. - State and features: new state belongs in the type's
DeviceStatesubclass. New public methods belong on the base class and are gated by a feature flag in the device map. - Test responses: add the device's
get_details()response toDETAILS_RESPONSESinsrc/tests/call_json_<type>.py, plus any method-specific responses inMETHOD_RESPONSES. - Test methods: add device-specific methods to
device_methodsinsrc/tests/test_<type>.py. - Fixtures: record the requests with
pytest --write_api, check the YAML against the capture, and rerunpytestwithout flags. - Docs: add a
:::block for the class indocs/devices/<type>.md, and list the model indocs/supported_devices.mdand the README device list. - Verify on hardware and state the model and firmware in the PR.
Pull Request Process↲
Semantic PR Titles↲
Pull request titles must follow the Conventional Commits format. This is enforced by the Semantic Title GitHub Action on all PRs. Valid prefixes:
feat:: new feature or device supportfix:: bug fixdocs:: documentation changesrefactor:: code refactoring (no functional change)test:: adding or updating testschore:: maintenance, dependency updates, CI changes
Examples:
feat: Add support for LAP-C601S air purifier
fix: Handle token expiration during device update
docs: Update contributing guidelines
What Happens on a PR↲
When you open a pull request targeting dev or master, the Run Linting and
Unit Tests workflow runs:
- Ruff:
ruff check --output-format=github src/pyvesync - Pylint:
pylint src/pyvesync - Pytest: the full test suite on Python 3.11, 3.12, 3.13, and 3.14
- MkDocs build: only on PRs to
master(Python 3.12)
The workflow fails if any of these steps fail. Separately:
- pre-commit.ci runs the pre-commit hooks, including mypy. mypy is not part of the test workflow, so run it locally.
- Semantic Title checks the PR title.
A maintainer then reviews the change against the Contribution Rules and the PR checklist.
Code Style↲
Ruff Configuration↲
The project uses 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)
- Scope:
src/pyvesync. Thesrc/testsdirectory is excluded from ruff and pre-commit.
Ruff runs with auto-fix enabled in pre-commit, so many issues are corrected
automatically on commit. mypy (mypy.ini), pylint (.pylintrc) and flake8
(setup.cfg) use the same 90-character limit.
General Style Guidelines↲
-
Quotes: Single quotes for all strings. Double quotes for docstrings.
-
String formatting: Use f-strings, except in logging calls, which use
%sarguments. -
Type hints: Required for all function signatures. Use
|union syntax, notUnionorOptional. Usefrom __future__ import annotationsat the top of every module. -
TYPE_CHECKING imports: Imports used only for type hints should be guarded behind
if TYPE_CHECKING:to avoid circular imports at runtime. -
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. -
Docstrings: Required for all public classes, methods, and functions. Use Google-style format. Not required for inherited/overridden methods.
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,OutletStateMethods / functions snake_case turn_on(),set_brightness()Properties snake_case device_status,fan_levelConstants UPPER_SNAKE_CASE DEFAULT_TZ,STATUS_OKEnums PascalCase class, UPPER members DeviceStatus.ONModule-level logger logger(preferred)logger = logging.getLogger(__name__) -
Constants: All constants, default values, and device modes must be defined in the
pyvesync.constmodule. No hardcoded strings or magic numbers in device code. UseStrEnumorIntEnumfor enum values. -
__slots__: Used on state classes, device classes, and theVeSyncmanager 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
DeviceStatesubclass in the base device type module. For example,SwitchStatefor switches,PurifierStatefor purifiers, etc. - All device properties and methods are to be created in the specific device type base class, not in the implementation device class.
-
Binary state methods follow this naming pattern:
Pattern Usage Example turn_on()/turn_off()Power on/off Inherited from VeSyncBaseToggleDeviceturn_<state>_on()/turn_<state>_off()Named binary state turn_child_lock_on(),turn_child_lock_off()toggle_<state>(bool)Toggle binary state toggle_child_lock(),toggle_display()set_<attribute>(value)Set non-binary state set_brightness(),set_fan_level() -
The
turn_on()andturn_off()methods are specific to power and call thetoggle_switch()method internally. -
Base-class methods that a concrete device may not implement use a stub pattern. They discard unused arguments with
del, log at debug level whether the feature is unsupported or just not configured, and returnFalse. Concrete classes that support the feature override the method. -
Concrete device classes declare
__slots__ = ().
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). Request models
subclass RequestBaseModel and response models subclass ResponseBaseModel
from models/base_models.py.
Testing and Linting↲
Running Tests Locally↲
# Run all tests
pytest
# Run a specific test file
pytest src/tests/test_outlets.py
# Run the tests for one device
pytest src/tests/test_outlets.py -k "ESW15-USA"
Recording API Fixtures↲
Device tests compare each request the library sends with a YAML fixture in
src/tests/api/<device module>/<setup_entry>.yaml. A missing fixture makes the
test fail, so new devices and methods need their fixtures recorded:
# Write fixtures that don't exist yet (existing fixtures are untouched)
pytest src/tests/test_outlets.py --write_api
# Rewrite existing fixtures - only when a request has intentionally changed
pytest src/tests/test_outlets.py --write_api --overwrite
Both flags ask for confirmation before writing. In a non-interactive shell (CI,
scripts, AI agents) set PYTEST_CONFIRM=1 to skip the prompt, otherwise the run
aborts. Narrow these runs to the module you changed, and review the YAML diff
before committing. Never edit fixtures by hand or overwrite them just to make a
failing test pass.
Running with Tox↲
For convenience, tox can be used to run tests and linting. This requires tox to be installed in your Python environment.
# 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
# Pass arguments to pytest after --
tox -e 3.12 -- src/tests/test_outlets.py --write_api
Running Linters Directly↲
ruff check src/pyvesync # Lint
ruff format src/pyvesync # Format
mypy src/pyvesync # Type check
pylint src/pyvesync # Pylint
See the Testing documentation and the tests README for details on the test architecture, fixtures, and adding tests for new devices.
Documentation↲
The documentation site is built with MkDocs and mkdocstrings. Most API reference pages are generated from docstrings, which is why public classes and methods need complete Google-style docstrings.
pip install -e .[docs]
mkdocs serve # live preview at http://127.0.0.1:8000
mkdocs build # what CI runs on PRs to master
- Device pages live in
docs/devices/<type>.md. Each device class is rendered by apyvesync.devices.<module>.<ClassName>block. Copy the options from a neighbouring block. - New devices are also listed in
docs/supported_devices.mdand the README. - Navigation is defined in
mkdocs.yml.
Release Process↲
Releases are triggered automatically when code is merged to master. The Release and Publish workflow:
- Extracts the version from
pyproject.toml - Validates the new version is greater than the latest git tag
- Builds the distribution package (
python -m build) - Creates a GitHub Release with auto-generated release notes and the version as the tag (e.g.,
3.4.1) - Publishes to PyPI via the
pypa/gh-action-pypi-publishaction - Deploys documentation using
miketo GitHub Pages, updating thelatestalias
Versioning↲
The project version is defined in pyproject.toml under [project].version. The version follows semantic versioning:
- 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 for version management. Each release
creates a versioned deployment and updates the latest alias. The documentation
site is at
webdjoe.github.io/pyvesync/latest.
Dependency Management↲
Dependabot is configured to check for updates weekly to both pip dependencies and GitHub Actions versions.
Requests to Add Devices↲
Please see Capturing for instructions on how to capture the information needed to add a new device, or how to share a device with a maintainer instead.