{"id":43206681,"url":"https://github.com/cmgrayb/libdyson-rest","last_synced_at":"2026-05-24T22:01:05.443Z","repository":{"id":310289354,"uuid":"1039327414","full_name":"cmgrayb/libdyson-rest","owner":"cmgrayb","description":"Python library for interacting with the Dyson REST API","archived":false,"fork":false,"pushed_at":"2026-05-19T01:17:25.000Z","size":603,"stargazers_count":0,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-19T02:44:38.935Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cmgrayb.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-08-17T01:12:08.000Z","updated_at":"2026-05-19T00:51:28.000Z","dependencies_parsed_at":null,"dependency_job_id":"9216aad3-c803-4eec-8bbc-7b8fa7b102d1","html_url":"https://github.com/cmgrayb/libdyson-rest","commit_stats":null,"previous_names":["cmgrayb/libdyson-rest"],"tags_count":88,"template":false,"template_full_name":null,"purl":"pkg:github/cmgrayb/libdyson-rest","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cmgrayb%2Flibdyson-rest","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cmgrayb%2Flibdyson-rest/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cmgrayb%2Flibdyson-rest/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cmgrayb%2Flibdyson-rest/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cmgrayb","download_url":"https://codeload.github.com/cmgrayb/libdyson-rest/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cmgrayb%2Flibdyson-rest/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33452033,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-24T19:21:36.376Z","status":"ssl_error","status_checked_at":"2026-05-24T19:21:10.562Z","response_time":57,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-02-01T07:00:50.875Z","updated_at":"2026-05-24T22:01:05.435Z","avatar_url":"https://github.com/cmgrayb.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# libdyson-rest\n\n[![PyPI version](https://badge.fury.io/py/libdyson-rest.svg)](https://badge.fury.io/py/libdyson-rest)\n[![Python](https://img.shields.io/pypi/pyversions/libdyson-rest.svg)](https://pypi.org/project/libdyson-rest/)\n[![License](https://img.shields.io/pypi/l/libdyson-rest.svg)](https://github.com/cmgrayb/libdyson-rest/blob/main/LICENSE)\n[![codecov](https://codecov.io/github/cmgrayb/libdyson-rest/branch/main/graph/badge.svg?token=NHV0GFO8FU)](https://codecov.io/github/cmgrayb/libdyson-rest)\n\nA Python library for interacting with Dyson devices through their official REST API.\n\n## Features\n\n- **Official API Compliance**: Implements the complete Dyson App API as documented in their OpenAPI specification\n- **Two-Step Authentication**: Secure login process with OTP codes via email or SMS (mobile)\n- **Mobile Authentication**: Support for SMS OTP authentication for China (CN) region users\n- **Complete Device Management**: List devices, get device details, and retrieve IoT credentials\n- **MQTT Connection Support**: Extract both cloud (AWS IoT) and local MQTT connection parameters\n- **Password Decryption**: Decrypt local MQTT broker credentials for direct device communication\n- **Token-Based Authentication**: Store and reuse authentication tokens for repeated API calls\n- **Type-Safe Models**: Comprehensive data models with proper type hints\n- **Error Handling**: Detailed exception hierarchy for robust error handling\n- **Context Manager Support**: Automatic resource cleanup\n- **Async/Await Support**: Full asynchronous client for Home Assistant and other async environments\n\n## Installation\n\nInstall from PyPI:\n\n```bash\npip install libdyson-rest\n```\n\nOr install from source:\n\n```bash\ngit clone https://github.com/cmgrayb/libdyson-rest.git\ncd libdyson-rest\npip install -e .\n```\n\n## Documentation\n\nFor comprehensive API documentation, see:\n\n- **[API Reference](docs/API.md)** - Complete method documentation for both sync and async clients\n- **[Examples](examples/)** - Practical usage examples and troubleshooting tools\n- **[Type Checking Guide](docs/STRICT_TYPE_CHECKING.md)** - Advanced type checking configuration\n- **[Modern Type Hints](docs/MODERN_TYPE_HINTS.md)** - Type hint patterns and best practices\n\n### Quick Reference\n\n| Client Type | Import | Context Manager | Best For |\n|------------|--------|-----------------|----------|\n| **Synchronous** | `from libdyson_rest import DysonClient` | `with DysonClient() as client:` | Scripts, simple applications |\n| **Asynchronous** | `from libdyson_rest import AsyncDysonClient` | `async with AsyncDysonClient() as client:` | Home Assistant, web servers, concurrent apps |\n\n## Quick Start\n\n### Synchronous Usage\n\n```python\nfrom libdyson_rest import DysonClient\n\n# Initialize the client\nclient = DysonClient(email=\"your@email.com\")\n\n# High-level authentication (recommended)\nif client.authenticate(\"123456\"):  # OTP code from email\n    devices = client.get_devices()\n    for device in devices:\n        print(f\"Device: {device.name} ({device.serial})\")\n        \nclient.close()\n```\n\n### Asynchronous Usage (Recommended for Home Assistant)\n\n```python\nimport asyncio\nfrom libdyson_rest import AsyncDysonClient\n\nasync def main():\n    async with AsyncDysonClient(email=\"your@email.com\") as client:\n        if await client.authenticate(\"123456\"):  # OTP code from email\n            devices = await client.get_devices()\n            for device in devices:\n                print(f\"Device: {device.name} ({device.serial})\")\n\nasyncio.run(main())\n```\n\n### Manual Authentication Flow\n\n```python\nfrom libdyson_rest import DysonClient\n\n# Initialize the client\nclient = DysonClient(\n    email=\"your@email.com\",\n    password=\"your_password\",\n    country=\"US\",        # ISO 3166-1 alpha-2 country code\n    culture=\"en-US\"      # IETF language code\n)\n\n# Two-step authentication process\ntry:\n    # Step 1: Begin login process\n    challenge = client.begin_login()\n    print(f\"Challenge ID: {challenge.challenge_id}\")\n    print(\"Check your email for an OTP code\")\n\n    # Step 2: Complete login with OTP code\n    otp_code = input(\"Enter OTP code: \")\n    login_info = client.complete_login(str(challenge.challenge_id), otp_code)\n    print(f\"Logged in! Account: {login_info.account}\")\n\n    # Get devices\n    devices = client.get_devices()\n    for device in devices:\n        print(f\"Device: {device.name} ({device.serial_number})\")\n        print(f\"  Type: {device.type}\")\n        print(f\"  Category: {device.category.value}\")\n\n        # Get IoT credentials for connected devices\n        if device.connection_category.value != \"nonConnected\":\n            iot_data = client.get_iot_credentials(device.serial_number)\n            print(f\"  IoT Endpoint: {iot_data.endpoint}\")\n\n        # Check for firmware updates\n        try:\n            pending_release = client.get_pending_release(device.serial_number)\n            print(f\"  Pending Firmware: {pending_release.version}\")\n            print(f\"  Update Pushed: {pending_release.pushed}\")\n        except Exception as e:\n            print(f\"  No pending firmware info available\")\n\nfinally:\n    client.close()\n```\n\n### Async/Await Usage (Recommended for Home Assistant)\n\n```python\nimport asyncio\nfrom libdyson_rest import AsyncDysonClient\n\nasync def main():\n    # Use async context manager for automatic cleanup\n    async with AsyncDysonClient(\n        email=\"your@email.com\",\n        password=\"your_password\",\n        country=\"US\",\n        culture=\"en-US\"\n    ) as client:\n        # Two-step authentication process\n        challenge = await client.begin_login()\n        print(f\"Challenge ID: {challenge.challenge_id}\")\n        print(\"Check your email for an OTP code\")\n\n        otp_code = input(\"Enter OTP code: \")\n        login_info = await client.complete_login(str(challenge.challenge_id), otp_code)\n        print(f\"Logged in! Account: {login_info.account}\")\n\n        # Get devices\n        devices = await client.get_devices()\n        for device in devices:\n            print(f\"Device: {device.name} ({device.serial_number})\")\n\n            # Get IoT credentials for connected devices\n            if device.connection_category.value != \"nonConnected\":\n                iot_data = await client.get_iot_credentials(device.serial_number)\n                print(f\"  IoT Endpoint: {iot_data.endpoint}\")\n\n# Run the async function\nasyncio.run(main())\n```\n\n## Authentication Flow\n\nThe Dyson API uses a secure two-step authentication process:\n\n### 1. API Provisioning (Automatic)\n```python\nversion = client.provision()  # Called automatically\n```\n\n### 2. User Status Check (Optional)\n```python\nuser_status = client.get_user_status()\nprint(f\"Account status: {user_status.account_status.value}\")\n```\n\n### 3. Begin Login Process\n```python\nchallenge = client.begin_login()\n# This triggers an OTP code to be sent to your email\n```\n\n### 4. Complete Login with OTP\n```python\nlogin_info = client.complete_login(\n    challenge_id=str(challenge.challenge_id),\n    otp_code=\"123456\"  # From your email\n)\n```\n\n### 5. Authenticated API Calls\n```python\ndevices = client.get_devices()\niot_data = client.get_iot_credentials(\"device_serial\")\n```\n\n## API Reference\n\n### DysonClient\n\n#### Constructor\n```python\nDysonClient(\n    email: Optional[str] = None,\n    password: Optional[str] = None,\n    country: str = \"US\",\n    culture: str = \"en-US\",\n    timeout: int = 30,\n    user_agent: str = \"android client\"\n)\n```\n\n#### Core Methods\n\n##### Authentication\n- `provision() -\u003e str`: Required initial API call\n- `get_user_status(email=None) -\u003e UserStatus`: Check account status\n- `begin_login(email=None) -\u003e LoginChallenge`: Start login process\n- `complete_login(challenge_id, otp_code, email=None, password=None) -\u003e LoginInformation`: Complete authentication\n- `authenticate(otp_code=None) -\u003e bool`: Convenience method for full auth flow\n\n##### Device Management\n- `get_devices() -\u003e List[Device]`: List all account devices\n- `get_iot_credentials(serial_number) -\u003e IoTData`: Get AWS IoT connection info\n- `get_pending_release(serial_number) -\u003e PendingRelease`: Get pending firmware release info\n\n##### Session Management\n- `close() -\u003e None`: Close session and clear state\n- `__enter__()` and `__exit__()`: Context manager support\n\n### AsyncDysonClient\n\nThe async client provides the same functionality as `DysonClient` but with async/await support for better performance in async environments like Home Assistant.\n\n#### Constructor\n```python\nAsyncDysonClient(\n    email: Optional[str] = None,\n    password: Optional[str] = None,\n    country: str = \"US\",\n    culture: str = \"en-US\",\n    timeout: int = 30,\n    user_agent: str = \"android client\"\n)\n```\n\n#### Core Methods (All Async)\n\n##### Authentication\n- `await provision() -\u003e str`: Required initial API call\n- `await get_user_status(email=None) -\u003e UserStatus`: Check account status\n- `await begin_login(email=None) -\u003e LoginChallenge`: Start login process\n- `await complete_login(challenge_id, otp_code, email=None, password=None) -\u003e LoginInformation`: Complete authentication\n- `await authenticate(otp_code=None) -\u003e bool`: Convenience method for full auth flow\n\n##### Device Management\n- `await get_devices() -\u003e List[Device]`: List all account devices\n- `await get_iot_credentials(serial_number) -\u003e IoTData`: Get AWS IoT connection info\n- `await get_pending_release(serial_number) -\u003e PendingRelease`: Get pending firmware release info\n\n##### Session Management\n- `await close() -\u003e None`: Close async session and clear state\n- `async with AsyncDysonClient() as client:`: Async context manager support\n\n**Note**: All methods except `decrypt_local_credentials()`, `get_auth_token()`, and `set_auth_token()` are async and must be awaited.\n\n### Data Models\n\n#### Device\n```python\n@dataclass\nclass Device:\n    category: DeviceCategory          # ec, flrc, hc, light, robot, wearable\n    connection_category: ConnectionCategory  # lecAndWifi, lecOnly, nonConnected, wifiOnly\n    model: str\n    name: str\n    serial_number: str\n    type: str\n    variant: Optional[str] = None\n    connected_configuration: Optional[ConnectedConfiguration] = None\n```\n\n#### DeviceCategory (Enum)\n- `ENVIRONMENT_CLEANER = \"ec\"` - Air filters, purifiers\n- `FLOOR_CLEANER = \"flrc\"` - Vacuum cleaners\n- `HAIR_CARE = \"hc\"` - Hair dryers, stylers\n- `LIGHT = \"light\"` - Lighting products\n- `ROBOT = \"robot\"` - Robot vacuums\n- `WEARABLE = \"wearable\"` - Wearable devices\n\n#### ConnectionCategory (Enum)\n- `LEC_AND_WIFI = \"lecAndWifi\"` - Bluetooth and Wi-Fi\n- `LEC_ONLY = \"lecOnly\"` - Bluetooth only\n- `NON_CONNECTED = \"nonConnected\"` - No connectivity\n- `WIFI_ONLY = \"wifiOnly\"` - Wi-Fi only\n\n#### LoginInformation\n```python\n@dataclass\nclass LoginInformation:\n    account: UUID      # Account ID\n    token: str         # Bearer token for API calls\n    token_type: TokenType  # Always \"Bearer\"\n```\n\n#### IoTData\n```python\n@dataclass\nclass IoTData:\n    endpoint: str              # AWS IoT endpoint\n    iot_credentials: IoTCredentials  # Connection credentials\n```\n\n#### PendingRelease\n```python\n@dataclass\nclass PendingRelease:\n    version: str     # Pending firmware version\n    pushed: bool     # Whether update has been pushed to device\n```\n\n### Exception Hierarchy\n\n```\nDysonAPIError (base)\n├── DysonConnectionError    # Network/connection issues\n├── DysonAuthError         # Authentication failures\n├── DysonDeviceError       # Device operation failures\n└── DysonValidationError   # Input validation errors\n```\n\n## Advanced Usage\n\n### Using Context Manager\n```python\nwith DysonClient(email=\"your@email.com\", password=\"password\") as client:\n    # Authentication\n    challenge = client.begin_login()\n    otp = input(\"Enter OTP: \")\n    client.complete_login(str(challenge.challenge_id), otp)\n\n    # API calls\n    devices = client.get_devices()\n    # Client automatically closed on exit\n```\n\n### Error Handling\n```python\nfrom libdyson_rest import DysonAuthError, DysonConnectionError, DysonAPIError\n\ntry:\n    client = DysonClient(email=\"user@example.com\", password=\"pass\")\n    challenge = client.begin_login()\n\nexcept DysonAuthError as e:\n    print(f\"Authentication failed: {e}\")\nexcept DysonConnectionError as e:\n    print(f\"Network error: {e}\")\nexcept DysonAPIError as e:\n    print(f\"API error: {e}\")\n```\n\n### Manual Authentication Steps\n```python\nclient = DysonClient(email=\"user@example.com\", password=\"password\")\n\n# Step 1: Provision (required)\nversion = client.provision()\nprint(f\"API version: {version}\")\n\n# Step 2: Check user status\nuser_status = client.get_user_status()\nprint(f\"Account active: {user_status.account_status.value == 'ACTIVE'}\")\n\n# Step 3: Begin login\nchallenge = client.begin_login()\nprint(\"Check email for OTP\")\n\n# Step 4: Complete login\notp = input(\"OTP: \")\nlogin_info = client.complete_login(str(challenge.challenge_id), otp)\nprint(f\"Bearer token: {login_info.token[:10]}...\")\n\n# Step 5: Use authenticated endpoints\ndevices = client.get_devices()\n```\n\n## Configuration\n\n### Environment Variables\n- `DYSON_EMAIL`: Default email address\n- `DYSON_PASSWORD`: Default password\n- `DYSON_MOBILE`: Mobile number with country code (for CN region mobile authentication, e.g., `+8613800000000`)\n- `DYSON_COUNTRY`: Default country code (default: \"US\")\n- `DYSON_CULTURE`: Default culture/locale (default: \"en-US\")\n- `DYSON_TIMEOUT`: Request timeout in seconds (default: \"30\")\n\n### Country and Culture Codes\n- **Country**: 2-letter uppercase ISO 3166-1 alpha-2 codes (e.g., \"US\", \"GB\", \"DE\")\n- **Culture**: 5-character IETF language codes (e.g., \"en-US\", \"en-GB\", \"de-DE\")\n\n### Regional API Endpoints\n\nThe library automatically selects the appropriate Dyson API endpoint based on your country code:\n\n| Country Code | Region | API Endpoint | Authentication Methods |\n|--------------|--------|--------------|------------------------|\n| `CN` | China | `https://appapi.cp.dyson.cn` | Email OTP, **Mobile SMS OTP** |\n| All others | Default | `https://appapi.cp.dyson.com` | Email OTP |\n\n**Examples:**\n```python\n# Chinese users - Email authentication\nclient = DysonClient(country=\"CN\")  # Uses appapi.cp.dyson.cn\n\n# Chinese users - Mobile authentication (SMS OTP)\nclient = DysonClient(\n    email=\"+8613800000000\",  # Mobile number with country code\n    password=\"your_password\",\n    country=\"CN\",\n    culture=\"zh-CN\"\n)\n# Call provision() first, then use mobile auth methods:\n# client.provision()\n# challenge = client.begin_login_mobile(\"+8613800000000\")\n# login_info = client.complete_login_mobile(challenge.challenge_id, otp_code, \"+8613800000000\")\n\n# All other users (US, UK, AU, NZ, etc.)\nclient = DysonClient(country=\"US\")  # Uses appapi.cp.dyson.com (default)\nclient = DysonClient(country=\"GB\")  # Uses appapi.cp.dyson.com (default)\n```\n\n**Mobile Authentication (CN Region Only)**:\n- Available only on China (CN) region server (`appapi.cp.dyson.cn`)\n- Uses SMS OTP codes instead of email OTP codes\n- Requires mobile number with country code prefix (e.g., `+8613800000000`)\n- Use dedicated mobile methods: `get_user_status_mobile()`, `begin_login_mobile()`, `complete_login_mobile()`\n- See [examples/mobile_auth_example.py](examples/mobile_auth_example.py) for complete usage\n\n**Note**: Regional endpoint selection is automatic and requires no code changes. Simply specify the correct country code for your region, and the library will route requests to the appropriate API server.\n\n## API Compliance\n\nThis library implements the complete Dyson App API as documented in their OpenAPI specification:\n- Authentication endpoints (`/v3/userregistration/email/*`)\n- Mobile authentication endpoints (`/v3/userregistration/mobile/*` - CN region only)\n- Device management (`/v3/manifest`)\n- IoT credentials (`/v2/authorize/iot-credentials`)\n- Provisioning (`/v1/provisioningservice/application/Android/version`)\n\n## Requirements\n\n- Python 3.10+\n- `requests` - HTTP client library\n- `dataclasses` - Data model support (Python 3.10+)\n\n## Contributing\n\nContributions are welcome! Please ensure all changes maintain compatibility with the official Dyson OpenAPI specification.\n\n## Versioning \u0026 Releases\n\nThis project follows **PEP 440** versioning (not semantic versioning). Here's how versions are distributed:\n\n### Version Patterns\n\n| Pattern | Example | Distribution | Purpose |\n|---------|---------|--------------|---------|\n| **Alpha** | `0.3.0a1`, `0.3.0alpha1` | TestPyPI | Internal testing only |\n| **Dev** | `0.3.0.dev1` | TestPyPI | Development builds |\n| **Beta** | `0.3.0b1`, `0.3.0beta1` | **PyPI** | Public beta testing |\n| **RC** | `0.3.0rc1` | **PyPI** | Release candidates |\n| **Stable** | `0.3.0` | **PyPI** | Production releases |\n| **Patch** | `0.3.0.post1` | **PyPI** | Post-release patches |\n\n### Installation\n\n```bash\n# Install stable release\npip install libdyson-rest\n\n# Install latest beta (includes rc, beta versions)\npip install --pre libdyson-rest\n\n# Install specific version\npip install libdyson-rest==0.7.0b1\n\n# Install from TestPyPI (alpha/dev versions)\npip install -i https://test.pypi.org/simple/ libdyson-rest==0.7.0a1\n```\n\n### For Beta Testers\n\nWant to help test new features? Install pre-release versions:\n\n```bash\npip install --pre libdyson-rest\n```\n\nThis will install the latest beta or release candidate, giving you access to new features before stable release.\n\n## License\n\nThis project is licensed under the MIT License - see the LICENSE file for details.\n\n## Disclaimer\n\nThis is an unofficial library. Dyson is a trademark of Dyson Ltd. This library is not affiliated with, endorsed by, or sponsored by Dyson Ltd.\n\n## OpenAPI Specification\n\nThis library is based on the community-documented Dyson App API OpenAPI specification. The specification can be found at:\nhttps://raw.githubusercontent.com/libdyson-wg/appapi/refs/heads/main/openapi.yaml\n\nThis project is created to further the efforts of others in the community in interacting with the\nDyson devices they have purchased to better integrate them into their smart homes.\n\nAt this time, this library is PURELY EXPERIMENTAL and should not be used without carefully examining\nthe code before doing so. **USE AT YOUR OWN RISK**\n\n## Features\n\n- Clean, intuitive API for Dyson device interaction\n- Full type hints support\n- Comprehensive error handling\n- Async/sync support\n- Built-in authentication handling\n- Extensive test coverage\n\n## Installation\n\n### From Source (Development)\n\n```bash\n# Clone the repository\ngit clone https://github.com/cmgrayb/libdyson-rest.git\ncd libdyson-rest\n\n# Create and activate virtual environment\npython -m venv .venv\n\n# Windows\n.venv\\Scripts\\activate\n\n# Linux/Mac\nsource .venv/bin/activate\n\n# Install development dependencies\npip install -r requirements-dev.txt\n```\n\n## Quick Start\n\n```python\nfrom libdyson_rest import DysonClient\n\n# Initialize the client\nclient = DysonClient(\n    email=\"your_email@example.com\",\n    password=\"your_password\",\n    country=\"US\"\n)\n\n# Authenticate with Dyson API\nclient.authenticate()\n\n# Get your devices\ndevices = client.get_devices()\nfor device in devices:\n    print(f\"Device: {device['name']} ({device['serial']})\")\n\n# Always close the client when done\nclient.close()\n\n# Or use as context manager\nwith DysonClient(email=\"email@example.com\", password=\"password\") as client:\n    client.authenticate()\n    devices = client.get_devices()\n    # Client is automatically closed\n```\n\n## Development\n\nThis project uses several tools to maintain code quality:\n\n- **Black**: Code formatting (120 character line length)\n- **Flake8**: Linting and style checking\n- **isort**: Import sorting\n- **MyPy**: Type checking\n- **Pytest**: Testing framework\n- **Pre-commit**: Git hooks\n\n### Setting up Development Environment\n\n1. **Create virtual environment and install dependencies:**\n   ```bash\n   python -m venv .venv\n   .venv\\Scripts\\activate  # Windows\n   pip install -r requirements-dev.txt\n   ```\n\n2. **Install pre-commit hooks:**\n   ```bash\n   pre-commit install\n   ```\n\n### VSCode Tasks\n\nThis project includes VSCode tasks for common development operations:\n\n- **Setup Dev Environment**: Create venv and install dependencies\n- **Format Code**: Run Black formatter\n- **Lint Code**: Run Flake8 linter\n- **Sort Imports**: Run isort\n- **Type Check**: Run MyPy type checker\n- **Run Tests**: Execute pytest with coverage\n- **Check All**: Run all quality checks in sequence\n\nAccess these via `Ctrl+Shift+P` → \"Tasks: Run Task\"\n\n### Code Quality Commands\n\n```bash\n# Format code\nblack .\n\n# Sort imports\nisort .\n\n# Lint code\nflake8 .\n\n# Type check\nmypy src/libdyson_rest\n\n# Run tests\npytest\n\n# Run all checks\nblack . \u0026\u0026 isort . \u0026\u0026 flake8 . \u0026\u0026 mypy src/libdyson_rest \u0026\u0026 pytest\n```\n\n### Testing\n\nRun tests with coverage:\n\n```bash\n# All tests\npytest\n\n# Unit tests only\npytest tests/unit/\n\n# Integration tests only\npytest tests/integration/\n\n# With coverage report\npytest --cov=src/libdyson_rest --cov-report=html\n```\n\n## Project Structure\n\n```\nlibdyson-rest/\n├── src/\n│   └── libdyson_rest/          # Main library code\n│       ├── __init__.py\n│       ├── client.py           # Main API client\n│       ├── exceptions.py       # Custom exceptions\n│       ├── models/             # Data models\n│       └── utils/              # Utility functions\n├── tests/\n│   ├── unit/                   # Unit tests\n│   └── integration/            # Integration tests\n├── .vscode/\n│   └── tasks.json             # VSCode tasks\n├── requirements.txt           # Production dependencies\n├── requirements-dev.txt       # Development dependencies\n├── pyproject.toml            # Project configuration\n├── .flake8                   # Flake8 configuration\n├── .pre-commit-config.yaml   # Pre-commit hooks\n└── README.md\n```\n\n## Configuration Files\n\n- **pyproject.toml**: Main project configuration (Black, isort, pytest, mypy)\n- **.flake8**: Flake8 linting configuration\n- **.pre-commit-config.yaml**: Git pre-commit hooks\n- **requirements.txt**: Production dependencies\n- **requirements-dev.txt**: Development dependencies\n\n## Publishing to PyPI\n\nThis package is automatically published to PyPI using GitHub Actions. For detailed publishing instructions, see [PUBLISHING.md](PUBLISHING.md).\n\n### Quick Publishing\n\n- **Test Release**: GitHub Actions → Run workflow → TestPyPI\n- **Production Release**: Create a GitHub release with version tag (e.g., `v0.2.0`)\n- **Local Build**: `python .github/scripts/publish_to_pypi.py --check`\n\nThe package is available on PyPI as `libdyson-rest`.\n\n## Contributing\n\n1. Fork the repository\n2. Create a feature branch: `git checkout -b feature-name`\n3. Make your changes following the coding standards\n4. Run all quality checks: ensure Black, Flake8, isort, MyPy, and tests pass\n5. Commit your changes: `git commit -am 'Add feature'`\n6. Push to the branch: `git push origin feature-name`\n7. Create a Pull Request\n\nAll PRs must pass the full test suite and code quality checks.\n\n## License\n\nThis project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.\n\n## Security\n\n- No hardcoded credentials or sensitive data\n- Use environment variables for configuration\n- All user inputs are validated\n- API responses are sanitized\n\n## Home Assistant Integration\n\nThis library is designed to work seamlessly with Home Assistant and other async Python environments. Use the `AsyncDysonClient` for optimal performance:\n\n```python\nimport asyncio\nfrom libdyson_rest import AsyncDysonClient\n\nclass DysonDeviceCoordinator:\n    \"\"\"Example Home Assistant coordinator pattern.\"\"\"\n    \n    def __init__(self, hass, email, password, auth_token=None):\n        self.hass = hass\n        self.client = AsyncDysonClient(\n            email=email,\n            password=password,\n            auth_token=auth_token\n        )\n    \n    async def async_update_data(self):\n        \"\"\"Update device data.\"\"\"\n        try:\n            devices = await self.client.get_devices()\n            return {device.serial_number: device for device in devices}\n        except Exception as err:\n            _LOGGER.error(\"Error updating Dyson devices: %s\", err)\n            raise UpdateFailed(f\"Error communicating with API: {err}\")\n    \n    async def async_get_iot_credentials(self, serial_number):\n        \"\"\"Get IoT credentials for MQTT connection.\"\"\"\n        return await self.client.get_iot_credentials(serial_number)\n    \n    async def async_close(self):\n        \"\"\"Close the client session.\"\"\"\n        await self.client.close()\n```\n\n### Performance Benefits\n\n- **Non-blocking I/O**: All HTTP requests are non-blocking\n- **Concurrent Operations**: Multiple device operations can run simultaneously\n- **Resource Efficient**: Proper async session management\n- **Home Assistant Ready**: Follows HA async patterns and best practices\n\n## Roadmap\n\n- [x] Complete API endpoint coverage\n- [x] Asynchronous client support\n- [ ] WebSocket real-time updates\n- [ ] Command-line interface\n- [ ] Docker container support\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcmgrayb%2Flibdyson-rest","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcmgrayb%2Flibdyson-rest","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcmgrayb%2Flibdyson-rest/lists"}