Skip to content
Merged
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
12 changes: 10 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
All notable changes to this project are recorded here. The format follows
Keep a Changelog; versions follow Semantic Versioning.

## Unreleased
## 1.0.0 - 2026-09-05

This is the first release of `python-broadlink`, a maintained fork of
`mjg59/python-broadlink` (PyPI `broadlink`, last released as 0.19.0). The
Expand All @@ -30,7 +30,15 @@ history below starts at that fork point.
- 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`.
- The CLI tools run their body under `asyncio.run`. `broadlink_cli
--learn` and `--rflearn` use `capture()` / `capture_rf()`, so a learning
session no longer goes deaf when the device times out partway through;
`--window` sets how long to listen, `--keep` prints every code heard, and
`--send --durations --repeat N` sets the repeat count. The CLI README's
`--rfscanlearn` was a typo for `--rflearn` (mjg59/python-broadlink#803,
#830).
- `pulses_to_data` returns `bytes` (it returned a `bytearray`, against its
own annotation).
- 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
Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ A Python module and CLI for controlling Broadlink devices locally.
> upstream [#839](https://github.com/mjg59/python-broadlink/issues/839)
> (fix in [#841](https://github.com/mjg59/python-broadlink/pull/841)) and
> adds the devices waiting in upstream's pull request queue, including the
> RM Max and RM5 Plus. Version 1.0 will be asynchronous; see `CHANGELOG.md`.
> Upstream's credit and MIT license are preserved.
> RM Max and RM5 Plus. Version 1.0 is asynchronous and adds `capture()`;
> see `CHANGELOG.md`. Upstream's credit and MIT license are preserved.

## Version 1.0 is asynchronous

Expand Down Expand Up @@ -166,9 +166,9 @@ await device.sweep_frequency()
2. When the LED blinks, point the remote at the Broadlink device for the first time and long press the button you want to learn.
3. Check if the frequency was successfully identified:
```python3
ok = device.check_frequency()
ok, frequency = await device.check_frequency()
if ok:
print('Frequency found!')
print(f'Frequency found: {frequency} MHz')
```
4. Enter learning mode:
```python3
Expand Down Expand Up @@ -251,12 +251,12 @@ await device.set_power(False)

### Checking power state
```python3
state = device.check_power()
state = await device.check_power()
```

### Checking energy consumption
```python3
state = device.get_energy()
state = await device.get_energy()
```

## Power strips
Expand All @@ -269,14 +269,14 @@ await device.set_power(1, False)

### Checking power state
```python3
state = device.check_power()
state = await device.check_power()
```

## Light bulbs

### Fetching data
```python3
state = device.get_state()
state = await device.get_state()
```

### Setting state attributes
Expand Down
26 changes: 22 additions & 4 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ This is a command line interface for the python-broadlink API.

Requirements
------------
You need to install the module first:
You need to install the module first (Python 3.13 or newer):
```
pip3 install broadlink
pip install python-broadlink
```

Installation
Expand Down Expand Up @@ -67,7 +67,13 @@ broadlink_cli --device @BEDROOM.device --learn

#### Learn RF code and show at console
```
broadlink_cli --device @BEDROOM.device --rfscanlearn
broadlink_cli --device @BEDROOM.device --rflearn
```
The device sweeps for the remote's carrier while you hold a button, then
learns the code from a short press. The sweep is unreliable on some
firmware; if you know the carrier, skip it:
```
broadlink_cli --device @BEDROOM.device --rflearn --frequency 433.92
```

#### Learn IR code and save to file
Expand All @@ -77,7 +83,14 @@ broadlink_cli --device @BEDROOM.device --learnfile LG-TV.power

#### Learn RF code and save to file
```
broadlink_cli --device @BEDROOM.device --rfscanlearn --learnfile LG-TV.power
broadlink_cli --device @BEDROOM.device --rflearn --learnfile LG-TV.power
```

#### Listen for longer, or for several codes
`--window` sets how many seconds to listen (default 30); `--keep` prints
every code heard during the window instead of stopping at the first:
```
broadlink_cli --device @BEDROOM.device --learn --window 120 --keep
```

#### Send code
Expand All @@ -90,6 +103,11 @@ broadlink_cli --device @BEDROOM.device --send DATA
broadlink_cli --device @BEDROOM.device --send @LG-TV.power
```

#### Send microsecond durations, repeated
```
broadlink_cli --device @BEDROOM.device --send --durations --repeat 2 +9000 -4500 +560 -560
```

#### Check temperature
```
broadlink_cli --device @BEDROOM.device --temperature
Expand Down
105 changes: 48 additions & 57 deletions cli/broadlink_cli
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
import argparse
import asyncio
import base64
import sys
import time
from contextlib import aclosing
from typing import List

import broadlink
from broadlink.const import DEFAULT_PORT
from broadlink.exceptions import ReadError, StorageError
from broadlink.remote import data_to_pulses, pulses_to_data
from broadlink.remote import CapturedSignal, data_to_pulses, pulses_to_data

TIMEOUT = 30

Expand All @@ -30,6 +31,35 @@ def parse_pulses(data: List[str]) -> List[int]:
return [abs(int(s)) for s in data]


def show(signal: CapturedSignal) -> None:
"""Print a captured signal in every format and save it if asked."""
raw_fmt = signal.packet.hex()
base64_fmt = base64.b64encode(signal.packet).decode('ascii')
pulse_fmt = format_pulses(signal.pulses)

print("Packet found!")
if signal.frequency_mhz:
print("Frequency: {}MHz".format(signal.frequency_mhz))
print("Raw:", raw_fmt)
print("Base64:", base64_fmt)
print("Pulses:", pulse_fmt)

if args.learnfile:
print("Saving to {}".format(args.learnfile))
with open(args.learnfile, "w") as text_file:
text_file.write(pulse_fmt if args.durations else raw_fmt)


async def listen(window) -> int:
"""Drain a capture window, printing each signal; return how many."""
heard = 0
async with aclosing(window) as signals:
async for signal in signals:
heard += 1
show(signal)
return heard


parser = argparse.ArgumentParser(fromfile_prefix_chars='@')
parser.add_argument("--device", help="device definition as 'type host mac'")
parser.add_argument("--type", type=auto_int, default=0x2712, help="type of device")
Expand All @@ -51,6 +81,12 @@ parser.add_argument("--learn", action="store_true", help="learn command")
parser.add_argument("--rflearn", action="store_true", help="rf scan learning")
parser.add_argument("--frequency", type=float, help="specify radiofrequency for learning")
parser.add_argument("--learnfile", help="save learned command to a specified file")
parser.add_argument("--window", type=float, default=TIMEOUT,
help="seconds to keep listening while learning (default %(default)s)")
parser.add_argument("--keep", action="store_true",
help="keep listening for the whole window and print every code heard")
parser.add_argument("--repeat", type=int, default=0,
help="with --send --durations: extra transmissions after the first")
parser.add_argument("--durations", action="store_true",
help="use durations in micro seconds instead of the Broadlink format")
parser.add_argument("--convert", action="store_true", help="convert input data to durations")
Expand Down Expand Up @@ -95,40 +131,17 @@ async def main():
print("{} {}".format(key, data[key]))
if args.send:
data = (
pulses_to_data(parse_pulses(args.data))
pulses_to_data(parse_pulses(args.data), repeat=args.repeat)
if args.durations
else bytes.fromhex(''.join(args.data))
)
await dev.send_data(data)
if args.learn or (args.learnfile and not args.rflearn):
await dev.enter_learning()
print("Learning...")
start = time.time()
while time.time() - start < TIMEOUT:
await asyncio.sleep(1)
try:
data = await dev.check_data()
except (ReadError, StorageError):
continue
else:
break
else:
heard = await listen(dev.capture(window=args.window, stop_after_first=not args.keep))
if not heard:
print("No data received...")
exit(1)

print("Packet found!")
raw_fmt = data.hex()
base64_fmt = base64.b64encode(data).decode('ascii')
pulse_fmt = format_pulses(data_to_pulses(data))

print("Raw:", raw_fmt)
print("Base64:", base64_fmt)
print("Pulses:", pulse_fmt)

if args.learnfile:
print("Saving to {}".format(args.learnfile))
with open(args.learnfile, "w") as text_file:
text_file.write(pulse_fmt if args.durations else raw_fmt)
sys.exit(1)
if args.check:
if await dev.check_power():
print('* ON *')
Expand Down Expand Up @@ -187,7 +200,7 @@ async def main():
else:
print("Radiofrequency not found")
await dev.cancel_sweep_frequency()
exit(1)
sys.exit(1)

print("Radiofrequency detected: {}MHz".format(frequency))
print("You can now let go of the button")
Expand All @@ -196,34 +209,12 @@ async def main():

print("Press the button again, now a short press.")

await dev.find_rf_packet(frequency)

start = time.time()
while time.time() - start < TIMEOUT:
await asyncio.sleep(1)
try:
data = await dev.check_data()
except (ReadError, StorageError):
continue
else:
break
else:
heard = await listen(
dev.capture_rf(window=args.window, frequency=frequency, stop_after_first=not args.keep)
)
if not heard:
print("No data received...")
exit(1)

print("Packet found!")
raw_fmt = data.hex()
base64_fmt = base64.b64encode(data).decode('ascii')
pulse_fmt = format_pulses(data_to_pulses(data))

print("Raw:", raw_fmt)
print("Base64:", base64_fmt)
print("Pulses:", pulse_fmt)

if args.learnfile:
print("Saving to {}".format(args.learnfile))
with open(args.learnfile, "w") as text_file:
text_file.write(pulse_fmt if args.durations else raw_fmt)
sys.exit(1)


if __name__ == "__main__":
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "python-broadlink"
version = "1.0.0.dev0"
version = "1.0.0"
description = "Python API for controlling Broadlink devices"
readme = "README.md"
license = "MIT"
Expand Down
Loading
Loading