Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 8 additions & 16 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
<!--
You are amazing! Thanks for contributing to our project!
Please fill the template to help maintainers processing your PR.
Thanks for contributing. Please fill in the sections below; it helps the
maintainer process your change.

Don't forget to create the PR against the correct branch:
- new product id -> new_product_ids
- anything else -> dev
Open the pull request against the master branch.
-->
## Context
<!--
Expand All @@ -21,8 +19,7 @@

## Type of change
<!--
What type of change does your PR introduce?
Please, check only 1 box!
Check one.
-->

- [ ] Dependency upgrade
Expand All @@ -41,15 +38,10 @@

- This PR fixes issue: fixes #
- This PR is related to:
- Link to documentation pull request:

## Checklist
<!--
Please do your best to check these boxes.
-->

- [ ] The code change is tested and works locally.
- [ ] The code has been formatted using Black.
- [ ] The code follows the [Zen of Python](https://www.python.org/dev/peps/pep-0020/).
- [ ] I am creating the Pull Request against the correct branch.
- [ ] Documentation added/updated.
- [ ] 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.
- [ ] `CHANGELOG.md` has an entry under Unreleased.
49 changes: 49 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: CI

on:
push:
branches: [master]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"
- name: Lint
run: ruff check .
- name: Test
run: pytest

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Build sdist and wheel
run: |
python -m pip install --upgrade pip build
python -m build
- name: Check the wheel imports
run: |
python -m venv /tmp/check
/tmp/check/bin/pip install dist/*.whl
/tmp/check/bin/python -c "import broadlink; print(broadlink.__name__)"
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
33 changes: 0 additions & 33 deletions .github/workflows/flake8.yaml

This file was deleted.

46 changes: 46 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Publish to PyPI

# Runs on a version tag (v1.0.0, v1.0.1, ...). Uses PyPI trusted publishing:
# the project on PyPI is configured to trust this repository, this workflow
# file name, and the "pypi" environment. No API token is stored anywhere.

on:
push:
tags:
- "v*"

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Check the tag matches the package version
run: |
TAG="${GITHUB_REF_NAME#v}"
VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
echo "tag=$TAG version=$VERSION"
test "$TAG" = "$VERSION"
- name: Build
run: |
python -m pip install --upgrade pip build
python -m build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/

publish:
needs: build
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,12 @@
*.pyc
__pycache__/
*.egg-info/
build/
dist/
.venv/
.pytest_cache/
.ruff_cache/
.DS_Store

# Working notes that are not part of the published project.
docs/internal/
90 changes: 90 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Changelog

All notable changes to this project are recorded here. The format follows
Keep a Changelog; versions follow Semantic Versioning.

## Unreleased

This is the first release of `python-broadlink`, a maintained fork of
`mjg59/python-broadlink` (PyPI `broadlink`, last released as 0.19.0). The
history below starts at that fork point.

### Changed

- **The library is asynchronous.** Every method that talks to a device is
now a coroutine: `await device.auth()`, `await device.send_data(...)`,
`await device.check_sensors()`, and so on. Discovery is
`await broadlink.discover(...)`, `broadlink.hello(...)` and `setup(...)`
are coroutines, and `xdiscover(...)` is an async generator. The packet
helpers (`pulses_to_data`, `data_to_pulses`), CRC and datetime helpers
stay synchronous. There is no synchronous compatibility layer: a call
without `await` returns a coroutine and does nothing.
- Each device keeps one UDP endpoint for its lifetime (the previous
version opened a socket per call) and serializes requests on it with an
`asyncio.Lock`. The old code declared a lock but never acquired it.
`async with device:` or `await device.aclose()` releases the endpoint;
it reopens on the next call.
- When a device reports that the session key has expired, the library
re-authenticates once and repeats the request. Callers no longer need
their own re-auth loop.
- Retry and timeout behaviour is unchanged: a request is repeated every
second until `timeout` elapses, then `NetworkTimeoutError` is raised.
- `dooya.set_percentage_and_wait` sleeps with `asyncio.sleep`.
- The CLI tools run their body under `asyncio.run`.
- Packaging moved to `pyproject.toml`; `setup.py` and the stale
`requirements.txt` pin are gone. The distribution name is now
`python-broadlink`; the import name stays `broadlink`. Python 3.13 or
newer is required.
- Continuous integration now runs `ruff` and `pytest` on Python 3.13 and
3.14, and builds the sdist and wheel on every pull request. Releases are
published to PyPI from version tags using trusted publishing.

### Fixed

- The IR tick constant used by `pulses_to_data` and `data_to_pulses` is now
`TICK = 8192 / 269` (about 30.45 us), matching the device's 32768 Hz
timebase as documented in `protocol.md`. The previous value, 32.84, was
the inverse ratio applied the wrong way round and compressed IR codes
built from true microsecond timings by about 7 percent. Codes learned and
replayed through the same device were unaffected. Verified on an RM4 Pro
against an independent receiver in both directions.
(mjg59/python-broadlink#839, #841)
- `pulses_to_data` rounds each duration to the nearest tick instead of
truncating, which removes up to one tick of systematic shortening per
pulse.

### Added

- `capture()` and `capture_rf()`, async generators that own the arm, poll,
timeout and re-arm loop of a learning session and yield each signal as a
`CapturedSignal` (device packet, decoded pulses at the correct tick,
kind, repeat count, and for RF the carrier frequency). They re-arm on a
timer, because the device leaves learning mode silently, and after any
`send_data`, because a transmission ends the session; both intervals and
the poll cadence were set from a bench on an RM4 Pro. Only one window can
be open per device. `capture_rf()` (Pro models only) takes the carrier
frequency directly and falls back to the on-device sweep when it is not
given.
- Packet helpers: `pulses_to_data` takes `kind` and `repeat`, `parse_packet`
is its inverse, and `SignalKind` names the IR, 433 MHz and 315 MHz bands.
A device's returned RF packet does not always use the canonical type byte
(an RM4 Pro answers a 433 MHz capture with 0xB1, not 0xB2), so the kind is
read by band and a capture is tagged from what it armed rather than the
byte.
- Devices, carried over from pull requests against the original repository
with their authors' commits intact: RM Max 0xAF8B (#838, Alexey Masolov);
RM5 plus 0x5224 with a new `rm5plus` class (#831, Anil Daoud); RM mini 3
OEM 0xA544 (#823, Bartłomiej Nogaś); RM mini 3 CMCC 0x27C8 (#802,
shuxin); LB26 R1 0xA517 (#812, techitapart); SP mini 3-AL 0x7D15 (#805,
bbcbbk); LEDVANCE SMART+ WIFI CEILING TW 24W 0x6498 (#799, Felipe Martins
Diel).
- Devices reported in issues against the original repository, added by
model name to the existing class for that family and not yet confirmed on
hardware: MP1-1K3S2U 0x4EDA (#816) and SP4 0xA57A (#758). Please open an
issue if either does not behave.
- `cryptography` 43 or newer is required, the first release with wheels for
Python 3.13 (supersedes mjg59/python-broadlink#749).
- A test suite. The `tests/oracle` package records the exact request bytes
every public method of every device class sends, and the results it
decodes from canned responses, so that later changes to the transport
can be checked byte for byte against the original behavior.
5 changes: 5 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
include LICENSE README.md CHANGELOG.md protocol.md TROUBLESHOOTING.md
include pyproject.toml
graft cli
graft tests
global-exclude __pycache__ *.py[cod] .DS_Store
Loading