Connection Management¶
Ephemeral vs. persistent connections¶
By default, AsyncDLightClient opens a new TCP connection for each command and closes it immediately after the response is received. This is the safest mode and requires no cleanup.
For applications that send many sequential commands, enable persistent connections. The client maintains a pool of open connections keyed by (host, port, ssl) and reuses them across calls.
Connection pool behaviour¶
The pool provides these guarantees:
- Per-device locking. Each
(host, port, ssl)key has its ownasyncio.Lock. Concurrent tasks sharing a client will queue up per device, not per client — so two lamps can be commanded in parallel while requests to the same lamp serialise. - Eviction on failure. Any exception during a send or receive evicts the connection immediately. If the connection was a reused persistent connection that went stale, the pool automatically discards it, opens a fresh connection, and transparently retries the failed command once. If the retry also fails or the connection was already brand new, the error is raised to the caller.
- Idle eviction. Connections unused for longer than
idle_timeout(default 60 s) are closed and removed from the pool.
The context manager¶
The async with pattern is the recommended lifecycle for persistent connections:
async with AsyncDLightClient(persistent=True) as client:
lamp = DLightDevice("192.168.1.123", "DL12345", client)
await lamp.turn_on()
await lamp.set_brightness(50)
# pool flushed; all TCP connections closed
The async with block does not enable persistence by itself — you must pass persistent=True. The context manager only guarantees that client.close() is called on exit, flushing any pooled connections.
Changed in 1.6.0
Prior to 1.6.0, entering the context manager implicitly set persistent=True. The current behaviour is explicit and predictable: persistence is a constructor flag, the context manager handles cleanup only.
Tuning idle timeout¶
If your application sends bursts of commands with long pauses between them, you may want to tune idle_timeout:
# Keep connections alive for 5 minutes between bursts
client = AsyncDLightClient(persistent=True, idle_timeout=300.0)
# Never evict on idle (not recommended — servers may close the socket anyway)
client = AsyncDLightClient(persistent=True, idle_timeout=0)
Setting idle_timeout=0 disables idle eviction. The connection will still be evicted on the next failure.
Retries¶
client = AsyncDLightClient(
max_retries=2, # attempt up to 2 retries (3 total attempts)
retry_backoff=0.5, # wait 0.5 s before first retry, 1.0 s before second
)
Retries fire on DLightTimeoutError and DLightConnectionError only. They never fire on DLightCommandError or DLightResponseError — those indicate a protocol-level problem that won't resolve by retrying.
Each retry gets a fresh TCP connection, even in persistent mode. A stale or broken connection is not re-used.
TLS¶
# Trust the server via the system CA store
client = AsyncDLightClient(ssl=True)
# Provide a custom context (self-signed certs, mutual TLS, etc.)
import ssl
ctx = ssl.create_default_context(cafile="/path/to/ca.pem")
client = AsyncDLightClient(ssl=ctx)
# Disable certificate verification (development / testing only)
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
client = AsyncDLightClient(ssl=ctx)
Concurrency¶
One AsyncDLightClient instance is safe to share across multiple coroutines and multiple DLightDevice objects. The per-device lock in the pool ensures that concurrent commands to the same lamp are serialised while commands to different lamps proceed in parallel:
async with AsyncDLightClient(persistent=True) as client:
lamp_a = DLightDevice("192.168.1.10", "DLA", client)
lamp_b = DLightDevice("192.168.1.11", "DLB", client)
# These run concurrently — different devices, different pool slots
await asyncio.gather(
lamp_a.set_brightness(100),
lamp_b.set_brightness(50),
)
Performance tips¶
| Scenario | Recommendation |
|---|---|
| Single command, fire-and-forget | Default (ephemeral) — simplest, no cleanup needed |
| Sequential burst of commands | persistent=True + context manager |
| Long-running automation / daemon | persistent=True, tune idle_timeout to match your command cadence |
| Unreliable Wi-Fi | Add max_retries=2, retry_backoff=0.5 |
| Production with device certificate | Pass a custom ssl.SSLContext |