Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
dbff472
Intro v2.0.0 with Async Architecture and remove Python 2 support
jasonacox Sep 14, 2025
aef972d
Add base class XenonDeviceAsync
3735943886 Sep 14, 2025
aae3e57
DeviceAsync class as base for v2 async support
3735943886 Sep 14, 2025
88b1deb
Restore some deleted comments, and quirks
uzlonewolf Sep 15, 2025
a2d363e
Move payload_dict/merge_dps_results out of Device, remove duplicated …
uzlonewolf Sep 15, 2025
8c56e27
Import find_device/device_info from XenonDevice
uzlonewolf Sep 15, 2025
1ca036f
Implement callbacks for data, connect, and dataless responses
uzlonewolf Sep 15, 2025
43e7dc5
Eliminate initialize() and create()
uzlonewolf Sep 15, 2025
ca31b82
Add initial implementation of test-devices.py for DeviceAsync regress…
jasonacox Sep 15, 2025
0d832a6
Merge branch 'v2-async' of https://github.com/jasonacox/tinytuya into…
jasonacox Sep 15, 2025
94b4bec
Enhance DeviceAsync status fetching with shared discovery and error c…
jasonacox Sep 15, 2025
28296d9
Add testing framework with pytest integration, coverage reporting, an…
jasonacox Sep 15, 2025
665bead
Add pytest-asyncio to dependencies in GitHub Actions workflow
jasonacox Sep 15, 2025
a282921
Rename examples
3735943886 Sep 15, 2025
6e3e213
minor bug fix
3735943886 Sep 17, 2025
acc4bb8
Delay certain callbacks and add missing disconnect cb
uzlonewolf Sep 20, 2025
fd6815f
Demote generate_payload to private method _generate_payload
uzlonewolf Sep 20, 2025
3553e34
Remove obsolete _send_receive() argument minresponse
uzlonewolf Sep 20, 2025
a527647
Add device lock and rework callbacks again
uzlonewolf Sep 20, 2025
d503ac5
Rework "connection closed while receiving" logic and add helpers star…
uzlonewolf Sep 20, 2025
4e29108
Update 'close' to '_close' in monkey test
uzlonewolf Sep 20, 2025
2e14cd9
Split _decrypt_payload() out of _decode_payload() and into own functi…
uzlonewolf Sep 20, 2025
4ee9253
Make sure _send_receive() always returns a dict (never None), and rew…
uzlonewolf Sep 20, 2025
39a045f
Completely remove nowait - all commands send, or receive, but never b…
uzlonewolf Sep 20, 2025
628fe64
Finish removing nowait
uzlonewolf Sep 20, 2025
f31f044
Allow passing custom timeouts to _receive, and move sending code to i…
uzlonewolf Sep 21, 2025
f1f29cb
Change defaults: persist is now True, version is now 3.5, and sendWai…
uzlonewolf Sep 21, 2025
2931d46
Delay closing non-persistent sockets for 100ms to give them a chance …
uzlonewolf Sep 21, 2025
d4f2e69
minor bug fix
3735943886 Sep 23, 2025
fd80b7b
Add continuous-receive helper and a receive lock
uzlonewolf Sep 23, 2025
37dfcb5
Pylint fix
uzlonewolf Sep 23, 2025
96d8147
Start of scanner. Only scanfor( dev_id, timeout ) works at the moment
uzlonewolf Sep 23, 2025
196c6ee
Fixes for scanner and v3.5
uzlonewolf Sep 23, 2025
621c81b
Make pylint ignore the scanner for now
uzlonewolf Sep 23, 2025
9f072d8
Skip locking if sending only and device is already connected
uzlonewolf Sep 23, 2025
446bcc4
Skip locking if send-only or receive-only and device is already conne…
uzlonewolf Sep 23, 2025
adde094
Rework device locking
uzlonewolf Sep 24, 2025
4cc040f
Rework callback running and task helpers
uzlonewolf Sep 26, 2025
c218311
Make send() private as well
uzlonewolf Sep 26, 2025
923eb5e
Fix test now that _send() is private
uzlonewolf Sep 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[run]
branch = True
source = tinytuya
omit =
*/__init__.py
tests/*
sandbox/*
build/*
setup.py
test.py

[report]
exclude_lines =
pragma: no cover
if __name__ == .__main__.
if TYPE_CHECKING:
pass
show_missing = True
2 changes: 1 addition & 1 deletion .github/workflows/pylint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ jobs:
pip install -r requirements.txt
- name: Analyzing the code with pylint
run: |
pylint --recursive y -E tinytuya/
pylint --recursive y -E --ignore scanner_async.py tinytuya/
27 changes: 24 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,28 @@ jobs:
python -VV
python -m site
python -m pip install --upgrade pip setuptools wheel
python -m pip install --upgrade cryptography requests colorama
python -m pip install --upgrade cryptography requests colorama pytest pytest-cov pytest-asyncio
# project editable install
pip install -e .

- name: "Run test.py and tests.py on ${{ matrix.python-version }}"
run: "python -m test.py && python -m tests"
- name: "Run legacy scripts"
run: python -m test.py && python -m tests

- name: "Run pytest with coverage"
run: pytest --cov=tinytuya --cov-report=xml --cov-report=term-missing

- name: "Upload coverage artifact"
uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.python-version }}
path: coverage.xml

- name: "Codecov upload"
if: matrix.python-version == '3.10'
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
flags: unittests
fail_ci_if_error: false
verbose: true
141 changes: 141 additions & 0 deletions ASYNC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# TinyTuya Async Roadmap (v2.x)

## Vision
Provide a first-class asyncio-native API for local Tuya LAN control that:
* Preserves rock-solid backward compatibility with the existing synchronous API (1.x style) for the large installed base.
* Enables high-concurrency, low-latency operations (parallel status polling, batched control, streaming updates) across mixed protocol versions (3.1–3.5) without blocking threads.
* Establishes a sustainable architecture so future protocol changes (e.g. 3.6+, new discovery flows) can be integrated once at the async layer and selectively backported.

## Goals
1. Async Core: Non-blocking socket connect, handshake, encrypt/decrypt, send/receive framed messages for all protocol versions.
2. High Throughput: Support dozens/hundreds of devices concurrently with graceful backpressure and timeout handling.
3. Pluggable Crypto & Parsing: Reuse existing message framing logic but allow async pipeline (reader / writer tasks) with cancellation.
4. Structured API: Mirror familiar synchronous class names with `Async` suffix (e.g. `XenonDeviceAsync`, `OutletDeviceAsync`).
5. Observability: Built-in debug / trace hooks and metrics counters (messages sent, retries, handshake duration) pluggable via callbacks.
6. Incremental Adoption: No forced migration—sync and async coexist; shared utility modules (e.g. encoding, DPS merge) remain single-source.

## Out of Scope
* Replacing synchronous classes or removing sync code paths.
* Full async Cloud API (could follow later).

## Architectural Overview (Planned)
```
+------------------------------+ +---------------------------+
| XenonDeviceAsync (base) | | MessageHelper (shared) |
| - state machine |<--calls--> | pack/unpack (sync funcs) |
| - connection supervisor | | crypto helpers |
| - protocol v3.1..v3.5 | +---------------------------+
| - send queue (asyncio.Queue)|
| - recv task (reader loop) | +---------------------------+
| - handshake coroutine |<--uses---->| Crypto (AESCipher) |
+--------------+---------------+ +---------------------------+
| derives
+----------+-----------+
| Async Device Mixins |
| (Outlet/Bulb/etc.) |
+----------------------+
```

## Milestones
| Milestone | Description | Deliverables | Target Version |
|-----------|-------------|--------------|----------------|
| M0 | Planning & Version Bump | v2.0.0, `ASYNC.md`, release notes | 2.0.0 |
| M1 | Async Core Skeleton | `xasync/connection.py`, `XenonDeviceAsync` minimal connect + status (3.1/3.3) | 2.1.0 |
| M2 | Protocol Coverage | Support 3.4/3.5 handshake & GCM in async path | 2.2.0 |
| M3 | Device Classes | `OutletDeviceAsync`, `BulbDeviceAsync`, `CoverDeviceAsync` parity subset | 2.3.0 |
| M4 | High-Perf Scanner | Async scanner refactor (parallel probes, cancellation) | 2.4.0 |
| M5 | Test & Metrics | 85%+ coverage for async modules; metrics hooks | 2.5.0 |
| M6 | Examples & Docs | Async examples, README + PROTOCOL cross-links | 2.6.0 |
| M7 | Optimization | Connection pooling, adaptive retry, rate limiting | 2.7.0 |

## Detailed Task Breakdown
### M1 – Async Core Skeleton
- [ ] Create package folder `tinytuya/asyncio/` (or `tinytuya/async_`) to avoid name collision.
- [ ] Implement `XenonDeviceAsync` with:
* `__init__(..., loop=None)` store config
* `_ensure_connection()` coroutine: open TCP, negotiate session key if needed
* `_reader_task()` coroutine: read frames, push to internal queue
* `_send_frame()` coroutine: pack + write
* `status()` -> `await get_status()` that sends DP_QUERY / CONTROL_NEW per version
* Graceful close / cancellation
- [ ] Reuse existing `pack_message` / `unpack_message` in a thread-safe way (they are CPU-bound but fast; optionally offload heavy crypto to default loop executor only if needed later).

### M2 – Protocol v3.4 / v3.5 Handshake
- [ ] Async handshake coroutine with timeout + auto-retry
- [ ] Session key caching per open connection
- [ ] Automatic renegotiation on GCM tag failure

### M3 – Device Class Parity
- [ ] Async mixins or subclasses replicating key sync API (`set_value`, `set_multiple_values`, `turn_on/off`)
- [ ] If return values differ (e.g. coroutines), document mapping in README
- [ ] Shared DPS merge logic factored into pure functions usable by both sync/async

### M4 – Async Scanner
- [ ] Coroutine to probe IP ranges concurrently (configurable concurrency)
- [ ] Cancel outstanding probes on shutdown
- [ ] Integrate UDP discovery (v3.1–v3.5) with async sockets
- [ ] Provide `await scan_network(subnet, timeout)` returning structured device list

### M5 – Tests & QA
- [ ] Pytest-asyncio test suite for: framing, handshake, reconnection, DPS updates
- [ ] Fake device server (async) to simulate v3.1, 3.3, 3.4, 3.5 behaviors
- [ ] Performance smoke test (N devices concurrently) gating PR merges

### M6 – Documentation & Examples
- [ ] `examples/async/` directory with: basic status, bulk control, scanner usage, bulb effects
- [ ] README section: “Using the Async API” with migration notes
- [ ] Cross-link PROTOCOL.md for handshake & framing details

### M7 – Optimization & Enhancements
- [ ] Connection pooling for multiple logical child devices (gateways) sharing transport
- [ ] Adaptive retry and exponential backoff for transient network errors
- [ ] Optional structured logging adapter (JSON events)
- [ ] Metrics hook interface (`on_event(event_name, **data)`) for integrations

## Testing Strategy
| Layer | Strategy |
|-------|----------|
| Unit | Pure functions (framing, header parse) deterministic tests |
| Integration | Async fake device endpoints per protocol version |
| Performance | Timed concurrent status for N synthetic devices (assert throughput baseline) |
| Regression | Mirror critical sync tests with async equivalents |
| Fuzz (future) | Random DP payload mutation on fake server to harden parsing |

## API Sketch (Draft)
```python
import asyncio
import tinytuya
from tinytuya.asyncio import XenonDeviceAsync

async def main():
dev = XenonDeviceAsync(dev_id, address=ip, local_key=key, version=3.5, persist=True)
status = await dev.status() # coroutine
print(status)
await dev.set_value(1, True)
await dev.close()

asyncio.run(main())
```
Methods returning coroutines (awaitables): `status`, `set_value`, `set_multiple_values`, `heartbeat`, `updatedps`, `close`.

## Backward Compatibility Plan
* Sync code paths untouched; existing imports remain default.
* Async lives under `tinytuya.asyncio` (explicit opt-in) to avoid polluting top-level namespace initially.
* When async reaches parity, consider promoting selected classes to top-level import in a minor release (opt-in alias only).

## Open Questions / To Refine
- Should we introduce an event callback API for spontaneous DP updates vs polling? (Likely yes in M3/M4.)
- Provide context manager (`async with XenonDeviceAsync(...)`) for auto-connect/close? (Planned.)
- Rate limiting: global vs per-device? (Investigate after baseline performance metrics.)

## Contribution Guidelines (Async Track)
* Prefer small, reviewable PRs per milestone task.
* Include tests & docs for every new public coroutine.
* Avoid breaking sync APIs—additive only.
* Mark experimental APIs with a leading `_` or mention in docstring.

## Next Step
Implement Milestone M1 skeleton: create async package, base class, minimal status() for 3.1 & 3.3 devices.

---
Maintained with the goal of long-term stability for existing users while enabling modern async performance.
22 changes: 22 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
# RELEASE NOTES

## v2.0.0 - Async Architecture Introduction (BREAKING MAJOR VERSION)

This major release introduces the foundation for native asyncio-based device communication while fully preserving the existing synchronous API for backward compatibility.

Highlights:
* Version bump to 2.x to signal new async subsystem (legacy sync classes unchanged).
* Planning document `ASYNC.md` added (vision, goals, milestones for XenonDeviceAsync & related classes).
* No behavioral changes to existing synchronous code paths in this initial 2.0.0 tag.
* Future minor releases (2.1.x+) will add new async classes and examples without removing sync support.

Compatibility:
* Existing imports and synchronous usage continue to work (API surface of 1.x retained).
* New async classes will live alongside current modules (no name collisions) and require explicit opt‑in.
* Officially removed Python 2.7 support.

Migration Guidance:
* You can adopt async incrementally—no action required if you stay with sync API.
* When async classes land, prefer `await device.status_async()` patterns in event loops for concurrency gains.

See `ASYNC.md` for roadmap details.


## 1.17.4 - Cloud Config

- Cloud: Add `configFile` option to the Cloud constructor, allowing users to specify the config file location (default remains 'tinytuya.json') by @blackw1ng in https://github.com/jasonacox/tinytuya/pull/640
Expand Down
27 changes: 27 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
codecov:
require_ci_to_pass: no

coverage:
precision: 2
round: down
range: 60..95
status:
project:
default:
target: auto
threshold: 2%
patch:
default:
target: auto
threshold: 2%

ignore:
- "sandbox/*"
- "tests/*"
- "build/*"
- "docs/*"

comment:
layout: "reach,diff,flags,files"
behavior: default
require_changes: false
6 changes: 3 additions & 3 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ Tested devices: Peteme Smart Light Bulbs, Wi-Fi - [link](https://www.amazon.com

[monitor.py](monitor.py) - This script uses a loop to listen to a Tuya device for any state changes.

## Async Send and Receive
## Non-blocking Send and Receive

[async_send_receive.py](async_send_receive.py) - This demonstrates how you can make a persistent connection to a Tuya device, send commands and monitor for responses in an async way.
[non_blocking_send_receive.py](non_blocking_send_receive.py) - This demonstrates how you can make a persistent connection to a Tuya device, send commands and monitor for responses without blocking.

## Send Raw DPS Values

Expand All @@ -49,7 +49,7 @@ turn_on('Dining Room')

## Multi-Threaded Example

[threading.py](threading.py) - Example that uses python threading to connect to multiple devices and listen for updates.
[multi-threading.py](multi-threading.py) - Example that uses python threading to connect to multiple devices and listen for updates.

## Multiple Device Select Example

Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# TinyTuya Example
# -*- coding: utf-8 -*-
"""
TinyTuya - Example showing async persistent connection to device with
TinyTuya - Example showing non-blocking persistent connection to device with
continual loop watching for device updates.

Author: Jason A. Cox
Expand Down
11 changes: 11 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[pytest]
# Ignore experimental or local work areas
norecursedirs = sandbox build dist .git .venv venv
testpaths = tests
python_files = test_*.py
asyncio_mode = auto
addopts = -ra --cov=tinytuya --cov-report=term-missing --cov-report=xml
markers =
asyncio: mark a test as using asyncio
integration: integration tests (may hit network)
slow: slow tests
1 change: 0 additions & 1 deletion server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
"""

# Modules
from __future__ import print_function
import threading
import time
import logging
Expand Down
Loading
Loading