Skip to content

Contributing to pyvesync

Contributions are welcome! Please follow the guidelines below to ensure a quick and smooth review process.

Getting Started

Install the Development Environment

# 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 to enforce code quality on every commit. The 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:

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 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:

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 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 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

  • Quotes: Single quotes for all strings. Double quotes for docstrings.

    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.

    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.

    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.

    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.

    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.

    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.

    # 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 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.
  • Binary state methods follow this naming pattern:

    Pattern Usage Example
    turn_on() / turn_off() Power on/off Inherited from VeSyncBaseToggleDevice
    turn_<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() 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

Running Tests Locally

# 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.

# 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

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 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:

  • 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 pyvesync.github.io.

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 necessary information to add a new device.