python-broadlink 1.0.5__tar.gz → 1.0.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/CHANGELOG.md +31 -4
  2. {python_broadlink-1.0.5/python_broadlink.egg-info → python_broadlink-1.0.6}/PKG-INFO +1 -1
  3. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/broadlink/__init__.py +3 -3
  4. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/broadlink/device.py +13 -1
  5. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/broadlink/remote.py +6 -4
  6. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/pyproject.toml +1 -1
  7. {python_broadlink-1.0.5 → python_broadlink-1.0.6/python_broadlink.egg-info}/PKG-INFO +1 -1
  8. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/tests/test_loopback.py +16 -0
  9. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/tests/test_transport.py +19 -0
  10. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/LICENSE +0 -0
  11. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/MANIFEST.in +0 -0
  12. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/README.md +0 -0
  13. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/TROUBLESHOOTING.md +0 -0
  14. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/broadlink/alarm.py +0 -0
  15. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/broadlink/climate.py +0 -0
  16. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/broadlink/const.py +0 -0
  17. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/broadlink/cover.py +0 -0
  18. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/broadlink/exceptions.py +0 -0
  19. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/broadlink/helpers.py +0 -0
  20. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/broadlink/hub.py +0 -0
  21. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/broadlink/light.py +0 -0
  22. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/broadlink/protocol.py +0 -0
  23. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/broadlink/sensor.py +0 -0
  24. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/broadlink/switch.py +0 -0
  25. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/cli/README.md +0 -0
  26. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/cli/broadlink_cli +0 -0
  27. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/cli/broadlink_discovery +0 -0
  28. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/protocol.md +0 -0
  29. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/python_broadlink.egg-info/SOURCES.txt +0 -0
  30. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/python_broadlink.egg-info/dependency_links.txt +0 -0
  31. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/python_broadlink.egg-info/requires.txt +0 -0
  32. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/python_broadlink.egg-info/top_level.txt +0 -0
  33. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/setup.cfg +0 -0
  34. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/tests/__init__.py +0 -0
  35. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/tests/oracle/__init__.py +0 -0
  36. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/tests/oracle/cases.py +0 -0
  37. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/tests/oracle/fixtures.json +0 -0
  38. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/tests/oracle/harness.py +0 -0
  39. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/tests/oracle/record.py +0 -0
  40. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/tests/test_capture.py +0 -0
  41. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/tests/test_helpers.py +0 -0
  42. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/tests/test_oracle.py +0 -0
  43. {python_broadlink-1.0.5 → python_broadlink-1.0.6}/tests/test_remote.py +0 -0
@@ -3,6 +3,32 @@
3
3
  All notable changes to this project are recorded here. The format follows
4
4
  Keep a Changelog; versions follow Semantic Versioning.
5
5
 
6
+ ## 1.0.6 - 2026-09-06
7
+
8
+ Small items from a sixth review, of 1.0.5, which found no defect in the
9
+ code. No change to the wire format or the public API.
10
+
11
+ ### Fixed
12
+
13
+ - A network failure during the library's own re-authentication (a timeout,
14
+ or the endpoint being closed) is raised as that failure. 1.0.3 to 1.0.5
15
+ returned the device's original expired-key answer instead, so the caller
16
+ raised `AuthorizationError` for what was really a timeout.
17
+ - An endpoint with no address on either side (discovery, `ping()`,
18
+ `setup()`) is bound to `0.0.0.0` explicitly, for the proactor loop on
19
+ Windows, which starts receiving as soon as the endpoint exists.
20
+ - The RM Max comment and the 1.0.0 changelog entry no longer claim more
21
+ than upstream #838 showed: the mapping follows that pull request's diff,
22
+ and its testers reported the device answering "locked" on
23
+ authentication, so it is listed as reported, not confirmed.
24
+
25
+ ### Changed
26
+
27
+ - An undecodable capture is logged at debug rather than warning, since the
28
+ window re-arms and carries on by itself.
29
+ - The RF capture loop closes its inner generator with `aclosing` like the
30
+ IR one.
31
+
6
32
  ## 1.0.5 - 2026-09-06
7
33
 
8
34
  Fixes from a fifth review, of 1.0.4. No change to the wire format or the
@@ -314,16 +340,17 @@ history below starts at that fork point.
314
340
  byte.
315
341
  - Devices, carried over from pull requests against the original repository
316
342
  with their authors credited (the changes were squash-merged with
317
- `Co-authored-by` trailers naming each author): RM Max 0xAF8B (#838, Alexey Masolov);
318
- RM5 plus 0x5224 with a new `rm5plus` class (#831, Anil Daoud); RM mini 3
343
+ `Co-authored-by` trailers naming each author): RM5 plus 0x5224 with a new `rm5plus` class (#831, Anil Daoud); RM mini 3
319
344
  OEM 0xA544 (#823, Bartłomiej Nogaś); RM mini 3 CMCC 0x27C8 (#802,
320
345
  shuxin); LB26 R1 0xA517 (#812, techitapart); SP mini 3-AL 0x7D15 (#805,
321
346
  bbcbbk); LEDVANCE SMART+ WIFI CEILING TW 24W 0x6498 (#799, Felipe Martins
322
347
  Diel).
323
348
  - Devices reported in issues against the original repository, added by
324
349
  model name to the existing class for that family and not yet confirmed on
325
- hardware: MP1-1K3S2U 0x4EDA (#816) and SP4 0xA57A (#758). Please open an
326
- issue if either does not behave.
350
+ hardware: MP1-1K3S2U 0x4EDA (#816) and SP4 0xA57A (#758), and the RM Max
351
+ 0xAF8B from #838 (Alexey Masolov, credited), whose testers reported the
352
+ device answering "locked" on authentication. Please open an issue if any
353
+ of them does not behave.
327
354
  - `cryptography` 43 or newer is required, the first release with wheels for
328
355
  Python 3.13 (supersedes mjg59/python-broadlink#749).
329
356
  - A test suite. The `tests/oracle` package records, for every public method
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-broadlink
3
- Version: 1.0.5
3
+ Version: 1.0.6
4
4
  Summary: Python API for controlling Broadlink devices
5
5
  Author: DAB-LABS
6
6
  Author-email: Matthew Garrett <mjg59@srcf.ucam.org>
@@ -116,9 +116,9 @@ SUPPORTED_TYPES = {
116
116
  0x27A6: ("RM plus", "Broadlink"),
117
117
  0x27A9: ("RM pro+", "Broadlink"),
118
118
  0x27C3: ("RM pro+", "Broadlink"),
119
- # The RM Max answers the RM pro framing; the RM4 framing (length
120
- # prefix) gets "device is locked" from it. Tested on hardware in
121
- # upstream #838, whose text says rm4pro but whose diff says rmpro.
119
+ # Mapping follows the diff in upstream #838 (its text says rm4pro,
120
+ # its diff says rmpro). Testers in that thread reported the device
121
+ # answering "locked" on auth, so treat it as reported, not confirmed.
122
122
  0xAF8B: ("RM Max", "Broadlink"),
123
123
  },
124
124
  rmminib: {
@@ -120,7 +120,14 @@ async def _open_endpoint(
120
120
  remote_addr: tuple[str, int] | None = None,
121
121
  broadcast: bool = False,
122
122
  ) -> tuple[asyncio.DatagramTransport, _Protocol]:
123
- """Create a UDP endpoint. Tests replace this to fake the network."""
123
+ """Create a UDP endpoint. Tests replace this to fake the network.
124
+
125
+ An endpoint with no address on either side is bound to ``0.0.0.0``
126
+ explicitly; the proactor loop on Windows starts receiving as soon as the
127
+ endpoint exists, which needs a bound socket.
128
+ """
129
+ if local_addr is None and remote_addr is None:
130
+ local_addr = ("0.0.0.0", 0)
124
131
  loop = asyncio.get_running_loop()
125
132
  transport, protocol = await loop.create_datagram_endpoint(
126
133
  _Protocol,
@@ -643,6 +650,11 @@ class Device:
643
650
  if self._auth_generation == generation:
644
651
  try:
645
652
  await self.auth()
653
+ except (e.NetworkTimeoutError, e.EndpointClosedError):
654
+ # A network failure during re-authentication is
655
+ # reported as what it is, not as the device's
656
+ # original expired-key answer.
657
+ raise
646
658
  except e.BroadlinkException as err:
647
659
  _LOGGER.debug(
648
660
  "%s: re-authentication failed: %s", self.host[0], err
@@ -408,7 +408,7 @@ class rmmini(Device):
408
408
  except ValueError as err:
409
409
  # A packet the device returned but we cannot decode. Log
410
410
  # it, re-arm and keep the window open.
411
- _LOGGER.warning(
411
+ _LOGGER.debug(
412
412
  "%s: ignoring an undecodable capture (%s): %s",
413
413
  self.host[0],
414
414
  err,
@@ -500,10 +500,12 @@ class rmpro(rmmini):
500
500
  raise ValueError("window must be 0 or positive, poll_interval positive")
501
501
  await self._claim_window()
502
502
  try:
503
- async for signal in self._capture_rf_body(
503
+ body = self._capture_rf_body(
504
504
  window, frequency, stop_after_first, poll_interval, rearm_interval
505
- ):
506
- yield signal
505
+ )
506
+ async with contextlib.aclosing(body):
507
+ async for signal in body:
508
+ yield signal
507
509
  finally:
508
510
  self._release_window()
509
511
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-broadlink"
7
- version = "1.0.5"
7
+ version = "1.0.6"
8
8
  description = "Python API for controlling Broadlink devices"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-broadlink
3
- Version: 1.0.5
3
+ Version: 1.0.6
4
4
  Summary: Python API for controlling Broadlink devices
5
5
  Author: DAB-LABS
6
6
  Author-email: Matthew Garrett <mjg59@srcf.ucam.org>
@@ -108,3 +108,19 @@ def test_endpoint_heals_after_a_socket_error_on_loopback(caplog):
108
108
 
109
109
  assert asyncio.run(go()) == 9
110
110
  assert "unreachable" in caplog.text
111
+
112
+
113
+ def test_unbound_endpoint_is_bound_explicitly():
114
+ """scan, ping and setup open an endpoint with no address on either
115
+ side; it is bound to 0.0.0.0 so it can receive on every platform."""
116
+
117
+ async def go():
118
+ transport, _ = await device_module._open_endpoint(broadcast=True)
119
+ try:
120
+ host, port = transport.get_extra_info("sockname")[:2]
121
+ return host, port
122
+ finally:
123
+ transport.close()
124
+
125
+ host, port = asyncio.run(go())
126
+ assert host == "0.0.0.0" and port > 0
@@ -631,6 +631,25 @@ def test_failed_reauth_returns_the_original_reply(net):
631
631
  e.check_error(resp[0x22:0x24])
632
632
 
633
633
 
634
+ def test_timeout_during_reauth_is_reported_as_a_timeout(net):
635
+ """If the device answers the request with an expired-key code and then
636
+ goes silent during the re-authentication, that is a network failure and
637
+ is raised as one, not returned as the device's original answer."""
638
+ dev = fixed_device()
639
+ dev.timeout = 0.03
640
+
641
+ async def go():
642
+ await dev._endpoint()
643
+ ep = net.endpoints[-1]
644
+ ep.replies = [(make_response(dev, b"", error=0xFFF9), HOST)] # then silence
645
+ with pytest.raises(e.NetworkTimeoutError):
646
+ await dev.send_packet(0x6A, b"")
647
+ return [int.from_bytes(f[0x26:0x28], "little") for f, _ in ep.sent]
648
+
649
+ types = run(go())
650
+ assert types[0] == 0x6A and set(types[1:]) == {0x65} # auth was tried and resent
651
+
652
+
634
653
  def test_locked_device_surfaces_as_the_original_error(net):
635
654
  """Device locked in the app: request answered -7, auth answered -1. The
636
655
  caller gets the -7 frame back (its check_error raises