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. The error is then raised to the caller.
- Liveness checked before reuse. Before a pooled connection is handed out it is checked for a departed peer — one that closed the connection cleanly, or reset it, while it sat idle. Such a connection is discarded and replaced transparently, without the caller noticing.
- Commands are never re-sent by the pool. If a failure survives the liveness check, it is raised to the caller. The pool does not re-send the command on a fresh connection, because only the caller can know whether re-sending is safe. See Retries below to opt into recovery.
- Idle eviction. Connections unused for longer than
idle_timeout(default 60 s) are closed and removed from the pool.
Changed in 2.1.0
Earlier versions re-sent the failed command once on a new connection when a reused connection failed. Because the client also retries, the two layers multiplied: a client with max_retries=2 could deliver one command to the lamp four times. The pool no longer re-sends anything.
Most stale connections are now caught before a command is written, so this is invisible. For the rest, persistent=True with the default max_retries=0 surfaces an error where it previously recovered silently — set max_retries=1 to restore the old effective behaviour, this time as your choice and with a bound you can predict.
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.
max_retries is the only retry counter, so the attempt count above is exact: max_retries=2 means the lamp receives the command at most three times. Nothing below this layer re-sends it.
Retries and non-idempotent commands
A retry re-sends a command that may already have reached the lamp — the protocol has no way to confirm delivery. For EXECUTE commands this is usually harmless because they set absolute values (brightness=40 twice leaves the lamp at 40). Raise max_retries deliberately, not by default.
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 |