Skip to content

Make the library asynchronous - #2

Merged
DAB-LABS merged 1 commit into
masterfrom
async-port
Sep 4, 2026
Merged

Make the library asynchronous#2
DAB-LABS merged 1 commit into
masterfrom
async-port

Conversation

@DAB-LABS

@DAB-LABS DAB-LABS commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Context

The reason this fork exists: Home Assistant wants an asynchronous Broadlink library. This is the port. Method names, arguments and return values are unchanged; every call that reaches a device is now a coroutine.

Proposed change

  • Device.send_packet, auth, hello, ping, get_fwversion, set_name, set_lock and every public method on every device class are async def. broadlink.discover, hello and setup are coroutines; xdiscover is an async generator. pulses_to_data, data_to_pulses, CRC16 and Datetime stay synchronous.
  • Transport: one asyncio.DatagramProtocol endpoint per device, opened on first use and kept; requests serialized with an asyncio.Lock. The old code opened a socket per call and its with self.lock and socket.socket(...) never acquired the lock. Retry every second until timeout, then NetworkTimeoutError, exactly as before. Stale datagrams are drained before each request. async with device: or await device.aclose() releases the endpoint.
  • If a device answers with an expired-session code (-7 or -4012) the library re-authenticates once and repeats the request; a second failure is raised as before.
  • Device classes: mechanical port. dooya.set_percentage_and_wait uses asyncio.sleep.
  • CLI: both tools run under asyncio.run.
  • README: a "Version 1.0 is asynchronous" section and every example updated. CHANGELOG entry.

Verification

  • The oracle suite from Add packaging, CI, publishing workflow and a byte-level test suite #1 passes with tests/oracle/fixtures.json untouched: all 155 cases send byte-identical requests and decode identical results to 0.19.0. That is the equivalence proof for the device classes, including the ones no one here can bench.
  • Transport tests rewritten against a fake endpoint; new cases for lock serialization (consecutive counters under gather), endpoint reuse, aclose and reopen, stale-reply draining, re-auth once, and no second re-auth.
  • Real endpoints exercised on the loopback (unbound broadcast, bound broadcast, connected) after a live run caught that create_datagram_endpoint needs an explicit address family for the unbound case.
  • Live against an RM4 Pro (firmware 52079) from a throwaway venv: hello 21 ms, auth 36 ms, firmware, sensors, three concurrent calls in 65 ms, enter_learning/check_data, send_data, LAN discovery, timeout on a dead address, and endpoint close.
  • 204 tests pass on Python 3.13 and 3.14.

Type of change

  • Breaking change (fix/feature causing existing functionality to break)

Additional information

Checklist

  • The code change is tested and works locally (pytest).
  • ruff check . passes.
  • New device support was verified on real hardware, or the PR says it was not. (No device changes; RM4 Pro used for the live check.)
  • CHANGELOG.md has an entry under Unreleased.

Every method that reaches a device is now a coroutine, with the same
names, arguments and return values as before. Discovery, hello and
setup are coroutines and xdiscover is an async generator. The packet,
CRC and datetime helpers stay synchronous. There is no synchronous
compatibility layer.

Transport: each device keeps one UDP endpoint (asyncio
DatagramProtocol) for its lifetime and serializes requests on it with
an asyncio.Lock; the previous code opened a socket per call and
declared a lock it never acquired. Retry and timeout behaviour is
unchanged. An expired session key is re-authenticated once and the
request repeated. async with / aclose() release the endpoint.

Device classes are a mechanical port (async def and await); the
oracle suite recorded in the previous change passes unchanged, so
every method sends the same bytes and decodes the same results as
0.19.0. Transport tests use a fake endpoint and gain cases for lock
serialization, endpoint reuse, stale-reply draining and re-auth.
The CLI runs under asyncio.run. README and CHANGELOG describe the
break.

Live-checked against an RM4 Pro: discovery, hello, auth, sensors,
concurrent calls, learning primitives, send, and the timeout path.
@DAB-LABS
DAB-LABS merged commit a2ae27f into master Sep 4, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant