python-broadlink 1.0.1__tar.gz → 1.0.2__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.
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/CHANGELOG.md +52 -0
- {python_broadlink-1.0.1/python_broadlink.egg-info → python_broadlink-1.0.2}/PKG-INFO +65 -12
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/README.md +64 -11
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/broadlink/__init__.py +17 -12
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/broadlink/alarm.py +1 -0
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/broadlink/climate.py +10 -27
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/broadlink/const.py +1 -0
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/broadlink/cover.py +2 -1
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/broadlink/device.py +95 -86
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/broadlink/exceptions.py +15 -3
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/broadlink/helpers.py +4 -3
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/broadlink/hub.py +8 -10
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/broadlink/light.py +31 -33
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/broadlink/protocol.py +1 -0
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/broadlink/remote.py +101 -59
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/broadlink/sensor.py +2 -1
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/broadlink/switch.py +32 -32
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/pyproject.toml +17 -4
- {python_broadlink-1.0.1 → python_broadlink-1.0.2/python_broadlink.egg-info}/PKG-INFO +65 -12
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/tests/oracle/cases.py +323 -72
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/tests/oracle/harness.py +3 -4
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/tests/test_capture.py +127 -24
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/tests/test_oracle.py +10 -2
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/tests/test_remote.py +2 -1
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/tests/test_transport.py +100 -3
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/LICENSE +0 -0
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/MANIFEST.in +0 -0
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/TROUBLESHOOTING.md +0 -0
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/cli/README.md +0 -0
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/cli/broadlink_cli +0 -0
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/cli/broadlink_discovery +0 -0
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/protocol.md +0 -0
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/python_broadlink.egg-info/SOURCES.txt +0 -0
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/python_broadlink.egg-info/dependency_links.txt +0 -0
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/python_broadlink.egg-info/requires.txt +0 -0
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/python_broadlink.egg-info/top_level.txt +0 -0
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/setup.cfg +0 -0
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/tests/__init__.py +0 -0
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/tests/oracle/__init__.py +0 -0
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/tests/oracle/fixtures.json +0 -0
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/tests/oracle/record.py +0 -0
- {python_broadlink-1.0.1 → python_broadlink-1.0.2}/tests/test_helpers.py +0 -0
|
@@ -3,6 +3,58 @@
|
|
|
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.2 - 2026-09-05
|
|
7
|
+
|
|
8
|
+
Fixes from a second, adversarial review of 1.0.1 and a re-test of the
|
|
9
|
+
first review's findings. No change to the wire format or the public API.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- 1.0.1's reply matching dropped a late reply to a request that had
|
|
14
|
+
timed out, but not the second reply to a request that was resent after a
|
|
15
|
+
silent second and then answered twice. That duplicate carries the counter
|
|
16
|
+
of a request that succeeded, and it could still be taken as the answer
|
|
17
|
+
to the next request. The library now remembers every recently used
|
|
18
|
+
counter and drops any reply carrying one other than the current
|
|
19
|
+
request's. A reply whose counter the device has not used recently is
|
|
20
|
+
still accepted, for firmware that may not echo it.
|
|
21
|
+
- `auth()` reset the session id and key before taking the request lock, so
|
|
22
|
+
a request already queued behind the lock could be framed with device id
|
|
23
|
+
0 and the initial key. The reset, the exchange and the install of the
|
|
24
|
+
new key now happen as one unit under the lock.
|
|
25
|
+
- 1.0.1 let a new capture window close one that a consumer had abandoned,
|
|
26
|
+
using "is the generator running right now" as the test. That cannot
|
|
27
|
+
tell an abandoned window from one whose consumer is awaiting something
|
|
28
|
+
between signals, which the README's own example does. A new window now
|
|
29
|
+
gives asyncio's finalizer one turn to close a genuinely dropped
|
|
30
|
+
generator and then refuses if the old window is still alive, rather
|
|
31
|
+
than taking it. A refused attempt no longer displaces the live window.
|
|
32
|
+
- A packet the device returned that cannot be decoded (a declared length
|
|
33
|
+
running into a truncated escape) no longer ends the capture window; it
|
|
34
|
+
is logged and the window re-arms.
|
|
35
|
+
- `aclose()` during a request now raises `EndpointClosedError`, a subclass
|
|
36
|
+
of `ConnectionClosedError` with code -4013 in the error table, so a
|
|
37
|
+
caller that closed the device on purpose can tell that apart from the
|
|
38
|
+
device's own "logged out" answer.
|
|
39
|
+
- `hello()` closes the discovery generator it breaks out of instead of
|
|
40
|
+
leaving the socket to the finalizer; `asyncio.TimeoutError` is spelled
|
|
41
|
+
`TimeoutError`; an unused future on the protocol object is gone.
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
- Debug logging on the `broadlink.device` and `broadlink.remote` loggers:
|
|
46
|
+
endpoint open and close, resends, dropped late replies, timeouts,
|
|
47
|
+
re-authentication, capture arm and re-arm, captured packets.
|
|
48
|
+
- README: a "Closing" section on the persistent socket, a "Timing" section
|
|
49
|
+
with the bench measurement of the tick fix (5.4 percent short before,
|
|
50
|
+
0.6 percent short after, on an RM4 Pro against an independent
|
|
51
|
+
receiver), a note that Python 3.13 is a support decision, and the short
|
|
52
|
+
list of return-value differences from 0.19.0.
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
|
|
56
|
+
- The code is formatted with `ruff format` and CI checks it.
|
|
57
|
+
|
|
6
58
|
## 1.0.1 - 2026-09-05
|
|
7
59
|
|
|
8
60
|
Fixes from an independent review of 1.0.0, most of them in the transport.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-broadlink
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: Python API for controlling Broadlink devices
|
|
5
5
|
Author: DAB-LABS
|
|
6
6
|
Author-email: Matthew Garrett <mjg59@srcf.ucam.org>
|
|
@@ -47,20 +47,25 @@ A Python module and CLI for controlling Broadlink devices locally.
|
|
|
47
47
|
|
|
48
48
|
## Version 1.0 is asynchronous
|
|
49
49
|
|
|
50
|
-
Every call that reaches a device is a coroutine and must be awaited.
|
|
51
|
-
is the
|
|
52
|
-
arguments and return values
|
|
50
|
+
Every call that reaches a device is a coroutine and must be awaited. That
|
|
51
|
+
is the main change from the original library's API: method names and
|
|
52
|
+
arguments are the same, and so are return values, with the small
|
|
53
|
+
exceptions listed in `CHANGELOG.md` (the IR tick constant, `pulses_to_data`
|
|
54
|
+
returning `bytes`, the unused `Device.lock` attribute removed, and
|
|
55
|
+
`timeout` parameters typed as floats).
|
|
53
56
|
|
|
54
57
|
```python
|
|
55
58
|
import asyncio
|
|
56
59
|
import broadlink
|
|
57
60
|
|
|
61
|
+
|
|
58
62
|
async def main():
|
|
59
63
|
devices = await broadlink.discover(timeout=5)
|
|
60
64
|
device = devices[0]
|
|
61
65
|
await device.auth()
|
|
62
66
|
print(await device.check_sensors())
|
|
63
67
|
|
|
68
|
+
|
|
64
69
|
asyncio.run(main())
|
|
65
70
|
```
|
|
66
71
|
|
|
@@ -83,8 +88,32 @@ The following devices are supported:
|
|
|
83
88
|
- **Thermostats**: Hysen HY02B05H
|
|
84
89
|
- **Hubs**: S3
|
|
85
90
|
|
|
91
|
+
## Timing
|
|
92
|
+
|
|
93
|
+
The original library converted microseconds to the device's timing units
|
|
94
|
+
with the constant 32.84, which is the right ratio applied the wrong way
|
|
95
|
+
round, and it shortened every IR code built from microsecond timings by
|
|
96
|
+
about 7 percent. Codes learned from a remote and replayed through the same
|
|
97
|
+
device were never affected, which is why it went unnoticed for years.
|
|
98
|
+
Version 1.0 uses 8192/269 (about 30.45 us per unit), the value implied by
|
|
99
|
+
`protocol.md`, and rounds to the nearest unit instead of truncating.
|
|
100
|
+
|
|
101
|
+
Measured on an RM4 Pro against an independent receiver, the same NEC frame
|
|
102
|
+
packed with the old constant arrived 5.4 percent short of its intended
|
|
103
|
+
length; packed with the corrected constant it arrived 0.6 percent short,
|
|
104
|
+
twice, thirteen hours apart, within 22 us of itself. Packets learned by
|
|
105
|
+
the device and replayed by name are unchanged. Anything that stores
|
|
106
|
+
microsecond timings produced by the old `data_to_pulses` (which reported
|
|
107
|
+
them about 7.8 percent long) and re-encodes them with the new
|
|
108
|
+
`pulses_to_data` will lengthen by that amount; store the device packet
|
|
109
|
+
instead, as `CapturedSignal.packet` does.
|
|
110
|
+
|
|
86
111
|
## Installation
|
|
87
112
|
|
|
113
|
+
Python 3.13 or newer. That is a support decision rather than a technical
|
|
114
|
+
one: the code runs on 3.11, but the versions tested in CI are 3.13 and
|
|
115
|
+
3.14 and those are the ones Home Assistant ships.
|
|
116
|
+
|
|
88
117
|
Use pip3 to install the latest version of this module.
|
|
89
118
|
|
|
90
119
|
```
|
|
@@ -125,7 +154,7 @@ In order to control the device, you need to connect it to your local network. If
|
|
|
125
154
|
- Manually connect to the WiFi SSID named BroadlinkProv.
|
|
126
155
|
2. Connect the device to your local network with the setup function.
|
|
127
156
|
```python3
|
|
128
|
-
await broadlink.setup(
|
|
157
|
+
await broadlink.setup("myssid", "mynetworkpass", 3)
|
|
129
158
|
```
|
|
130
159
|
|
|
131
160
|
Security mode options are (0 = none, 1 = WEP, 2 = WPA1, 3 = WPA2, 4 = WPA1/2)
|
|
@@ -134,7 +163,7 @@ Security mode options are (0 = none, 1 = WEP, 2 = WPA1, 3 = WPA2, 4 = WPA1/2)
|
|
|
134
163
|
|
|
135
164
|
You may need to specify a broadcast address if setup is not working.
|
|
136
165
|
```python3
|
|
137
|
-
await broadlink.setup(
|
|
166
|
+
await broadlink.setup("myssid", "mynetworkpass", 3, ip_address="192.168.0.255")
|
|
138
167
|
```
|
|
139
168
|
|
|
140
169
|
### Discovery
|
|
@@ -150,17 +179,17 @@ You may need to specify `local_ip_address` or `discover_ip_address` if discovery
|
|
|
150
179
|
|
|
151
180
|
Using the IP address of your local machine:
|
|
152
181
|
```python3
|
|
153
|
-
devices = await broadlink.discover(local_ip_address=
|
|
182
|
+
devices = await broadlink.discover(local_ip_address="192.168.0.100")
|
|
154
183
|
```
|
|
155
184
|
|
|
156
185
|
Using the broadcast address of your subnet:
|
|
157
186
|
```python3
|
|
158
|
-
devices = await broadlink.discover(discover_ip_address=
|
|
187
|
+
devices = await broadlink.discover(discover_ip_address="192.168.0.255")
|
|
159
188
|
```
|
|
160
189
|
|
|
161
190
|
If the device is locked, it may not be discoverable with broadcast. In such cases, you can use the unicast version `broadlink.hello()` for direct discovery:
|
|
162
191
|
```python3
|
|
163
|
-
device = await broadlink.hello(
|
|
192
|
+
device = await broadlink.hello("192.168.0.16")
|
|
164
193
|
```
|
|
165
194
|
|
|
166
195
|
If you are a perfomance freak, use `broadlink.xdiscover()` to create devices instantly:
|
|
@@ -175,6 +204,27 @@ After discovering the device, call the `auth()` method to obtain the authenticat
|
|
|
175
204
|
await device.auth()
|
|
176
205
|
```
|
|
177
206
|
|
|
207
|
+
### Closing
|
|
208
|
+
|
|
209
|
+
Each device keeps one UDP socket open for its lifetime (the original
|
|
210
|
+
library opened a new one for every call). Close it when you are done with
|
|
211
|
+
the device, either with the context manager or explicitly:
|
|
212
|
+
|
|
213
|
+
```python3
|
|
214
|
+
async with device:
|
|
215
|
+
await device.auth()
|
|
216
|
+
print(await device.check_sensors())
|
|
217
|
+
|
|
218
|
+
# or
|
|
219
|
+
await device.aclose()
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The socket reopens by itself on the next call, so closing is cheap and
|
|
223
|
+
safe to do at any time. A request that is in flight when `aclose()` runs
|
|
224
|
+
fails with `EndpointClosedError`. An integration that creates devices
|
|
225
|
+
should close them when it unloads; a device that is never closed holds
|
|
226
|
+
its socket until it is garbage collected.
|
|
227
|
+
|
|
178
228
|
The next steps depend on the type of device you want to control.
|
|
179
229
|
|
|
180
230
|
## Universal remotes
|
|
@@ -206,7 +256,7 @@ await device.sweep_frequency()
|
|
|
206
256
|
```python3
|
|
207
257
|
ok, frequency = await device.check_frequency()
|
|
208
258
|
if ok:
|
|
209
|
-
print(f
|
|
259
|
+
print(f"Frequency found: {frequency} MHz")
|
|
210
260
|
```
|
|
211
261
|
4. Enter learning mode:
|
|
212
262
|
```python3
|
|
@@ -248,10 +298,13 @@ By default the window closes after the first signal. Pass
|
|
|
248
298
|
`window=0` runs until the generator is closed), re-arming after each signal
|
|
249
299
|
because the device holds only one code per learning session. A universal
|
|
250
300
|
remote has a single receiver, so only one capture window can be open on a
|
|
251
|
-
device at a time
|
|
301
|
+
device at a time: opening a second one raises `CaptureInProgressError`
|
|
302
|
+
while the first is still held. Always close a window you leave early
|
|
303
|
+
(`aclosing` above does it), otherwise it stays open until Python collects
|
|
304
|
+
the generator.
|
|
252
305
|
|
|
253
306
|
`CapturedSignal` carries the device's own `packet` bytes (ready for
|
|
254
|
-
`send_data`), the decoded `pulses` in microseconds at the
|
|
307
|
+
`send_data`), the decoded `pulses` in microseconds at the corrected tick, the
|
|
255
308
|
`kind` (`SignalKind.IR`, `RF_433` or `RF_315`), the `repeat` count, and for
|
|
256
309
|
RF the `frequency_mhz` the packet itself does not record.
|
|
257
310
|
|
|
@@ -16,20 +16,25 @@ A Python module and CLI for controlling Broadlink devices locally.
|
|
|
16
16
|
|
|
17
17
|
## Version 1.0 is asynchronous
|
|
18
18
|
|
|
19
|
-
Every call that reaches a device is a coroutine and must be awaited.
|
|
20
|
-
is the
|
|
21
|
-
arguments and return values
|
|
19
|
+
Every call that reaches a device is a coroutine and must be awaited. That
|
|
20
|
+
is the main change from the original library's API: method names and
|
|
21
|
+
arguments are the same, and so are return values, with the small
|
|
22
|
+
exceptions listed in `CHANGELOG.md` (the IR tick constant, `pulses_to_data`
|
|
23
|
+
returning `bytes`, the unused `Device.lock` attribute removed, and
|
|
24
|
+
`timeout` parameters typed as floats).
|
|
22
25
|
|
|
23
26
|
```python
|
|
24
27
|
import asyncio
|
|
25
28
|
import broadlink
|
|
26
29
|
|
|
30
|
+
|
|
27
31
|
async def main():
|
|
28
32
|
devices = await broadlink.discover(timeout=5)
|
|
29
33
|
device = devices[0]
|
|
30
34
|
await device.auth()
|
|
31
35
|
print(await device.check_sensors())
|
|
32
36
|
|
|
37
|
+
|
|
33
38
|
asyncio.run(main())
|
|
34
39
|
```
|
|
35
40
|
|
|
@@ -52,8 +57,32 @@ The following devices are supported:
|
|
|
52
57
|
- **Thermostats**: Hysen HY02B05H
|
|
53
58
|
- **Hubs**: S3
|
|
54
59
|
|
|
60
|
+
## Timing
|
|
61
|
+
|
|
62
|
+
The original library converted microseconds to the device's timing units
|
|
63
|
+
with the constant 32.84, which is the right ratio applied the wrong way
|
|
64
|
+
round, and it shortened every IR code built from microsecond timings by
|
|
65
|
+
about 7 percent. Codes learned from a remote and replayed through the same
|
|
66
|
+
device were never affected, which is why it went unnoticed for years.
|
|
67
|
+
Version 1.0 uses 8192/269 (about 30.45 us per unit), the value implied by
|
|
68
|
+
`protocol.md`, and rounds to the nearest unit instead of truncating.
|
|
69
|
+
|
|
70
|
+
Measured on an RM4 Pro against an independent receiver, the same NEC frame
|
|
71
|
+
packed with the old constant arrived 5.4 percent short of its intended
|
|
72
|
+
length; packed with the corrected constant it arrived 0.6 percent short,
|
|
73
|
+
twice, thirteen hours apart, within 22 us of itself. Packets learned by
|
|
74
|
+
the device and replayed by name are unchanged. Anything that stores
|
|
75
|
+
microsecond timings produced by the old `data_to_pulses` (which reported
|
|
76
|
+
them about 7.8 percent long) and re-encodes them with the new
|
|
77
|
+
`pulses_to_data` will lengthen by that amount; store the device packet
|
|
78
|
+
instead, as `CapturedSignal.packet` does.
|
|
79
|
+
|
|
55
80
|
## Installation
|
|
56
81
|
|
|
82
|
+
Python 3.13 or newer. That is a support decision rather than a technical
|
|
83
|
+
one: the code runs on 3.11, but the versions tested in CI are 3.13 and
|
|
84
|
+
3.14 and those are the ones Home Assistant ships.
|
|
85
|
+
|
|
57
86
|
Use pip3 to install the latest version of this module.
|
|
58
87
|
|
|
59
88
|
```
|
|
@@ -94,7 +123,7 @@ In order to control the device, you need to connect it to your local network. If
|
|
|
94
123
|
- Manually connect to the WiFi SSID named BroadlinkProv.
|
|
95
124
|
2. Connect the device to your local network with the setup function.
|
|
96
125
|
```python3
|
|
97
|
-
await broadlink.setup(
|
|
126
|
+
await broadlink.setup("myssid", "mynetworkpass", 3)
|
|
98
127
|
```
|
|
99
128
|
|
|
100
129
|
Security mode options are (0 = none, 1 = WEP, 2 = WPA1, 3 = WPA2, 4 = WPA1/2)
|
|
@@ -103,7 +132,7 @@ Security mode options are (0 = none, 1 = WEP, 2 = WPA1, 3 = WPA2, 4 = WPA1/2)
|
|
|
103
132
|
|
|
104
133
|
You may need to specify a broadcast address if setup is not working.
|
|
105
134
|
```python3
|
|
106
|
-
await broadlink.setup(
|
|
135
|
+
await broadlink.setup("myssid", "mynetworkpass", 3, ip_address="192.168.0.255")
|
|
107
136
|
```
|
|
108
137
|
|
|
109
138
|
### Discovery
|
|
@@ -119,17 +148,17 @@ You may need to specify `local_ip_address` or `discover_ip_address` if discovery
|
|
|
119
148
|
|
|
120
149
|
Using the IP address of your local machine:
|
|
121
150
|
```python3
|
|
122
|
-
devices = await broadlink.discover(local_ip_address=
|
|
151
|
+
devices = await broadlink.discover(local_ip_address="192.168.0.100")
|
|
123
152
|
```
|
|
124
153
|
|
|
125
154
|
Using the broadcast address of your subnet:
|
|
126
155
|
```python3
|
|
127
|
-
devices = await broadlink.discover(discover_ip_address=
|
|
156
|
+
devices = await broadlink.discover(discover_ip_address="192.168.0.255")
|
|
128
157
|
```
|
|
129
158
|
|
|
130
159
|
If the device is locked, it may not be discoverable with broadcast. In such cases, you can use the unicast version `broadlink.hello()` for direct discovery:
|
|
131
160
|
```python3
|
|
132
|
-
device = await broadlink.hello(
|
|
161
|
+
device = await broadlink.hello("192.168.0.16")
|
|
133
162
|
```
|
|
134
163
|
|
|
135
164
|
If you are a perfomance freak, use `broadlink.xdiscover()` to create devices instantly:
|
|
@@ -144,6 +173,27 @@ After discovering the device, call the `auth()` method to obtain the authenticat
|
|
|
144
173
|
await device.auth()
|
|
145
174
|
```
|
|
146
175
|
|
|
176
|
+
### Closing
|
|
177
|
+
|
|
178
|
+
Each device keeps one UDP socket open for its lifetime (the original
|
|
179
|
+
library opened a new one for every call). Close it when you are done with
|
|
180
|
+
the device, either with the context manager or explicitly:
|
|
181
|
+
|
|
182
|
+
```python3
|
|
183
|
+
async with device:
|
|
184
|
+
await device.auth()
|
|
185
|
+
print(await device.check_sensors())
|
|
186
|
+
|
|
187
|
+
# or
|
|
188
|
+
await device.aclose()
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The socket reopens by itself on the next call, so closing is cheap and
|
|
192
|
+
safe to do at any time. A request that is in flight when `aclose()` runs
|
|
193
|
+
fails with `EndpointClosedError`. An integration that creates devices
|
|
194
|
+
should close them when it unloads; a device that is never closed holds
|
|
195
|
+
its socket until it is garbage collected.
|
|
196
|
+
|
|
147
197
|
The next steps depend on the type of device you want to control.
|
|
148
198
|
|
|
149
199
|
## Universal remotes
|
|
@@ -175,7 +225,7 @@ await device.sweep_frequency()
|
|
|
175
225
|
```python3
|
|
176
226
|
ok, frequency = await device.check_frequency()
|
|
177
227
|
if ok:
|
|
178
|
-
print(f
|
|
228
|
+
print(f"Frequency found: {frequency} MHz")
|
|
179
229
|
```
|
|
180
230
|
4. Enter learning mode:
|
|
181
231
|
```python3
|
|
@@ -217,10 +267,13 @@ By default the window closes after the first signal. Pass
|
|
|
217
267
|
`window=0` runs until the generator is closed), re-arming after each signal
|
|
218
268
|
because the device holds only one code per learning session. A universal
|
|
219
269
|
remote has a single receiver, so only one capture window can be open on a
|
|
220
|
-
device at a time
|
|
270
|
+
device at a time: opening a second one raises `CaptureInProgressError`
|
|
271
|
+
while the first is still held. Always close a window you leave early
|
|
272
|
+
(`aclosing` above does it), otherwise it stays open until Python collects
|
|
273
|
+
the generator.
|
|
221
274
|
|
|
222
275
|
`CapturedSignal` carries the device's own `packet` bytes (ready for
|
|
223
|
-
`send_data`), the decoded `pulses` in microseconds at the
|
|
276
|
+
`send_data`), the decoded `pulses` in microseconds at the corrected tick, the
|
|
224
277
|
`kind` (`SignalKind.IR`, `RF_433` or `RF_315`), the `repeat` count, and for
|
|
225
278
|
RF the `frequency_mhz` the packet itself does not record.
|
|
226
279
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""The python-broadlink library."""
|
|
3
|
+
|
|
4
|
+
import contextlib
|
|
3
5
|
from collections.abc import AsyncIterator
|
|
4
|
-
from typing import
|
|
6
|
+
from typing import Optional, Union
|
|
5
7
|
|
|
6
8
|
from . import exceptions as e
|
|
7
9
|
from .alarm import S1C
|
|
@@ -223,8 +225,8 @@ SUPPORTED_TYPES = {
|
|
|
223
225
|
|
|
224
226
|
def gendevice(
|
|
225
227
|
dev_type: int,
|
|
226
|
-
host:
|
|
227
|
-
mac:
|
|
228
|
+
host: tuple[str, int],
|
|
229
|
+
mac: bytes | str,
|
|
228
230
|
name: str = "",
|
|
229
231
|
is_locked: bool = False,
|
|
230
232
|
) -> Device:
|
|
@@ -258,12 +260,15 @@ async def hello(
|
|
|
258
260
|
|
|
259
261
|
Useful if the device is locked.
|
|
260
262
|
"""
|
|
261
|
-
async
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
263
|
+
async with contextlib.aclosing(
|
|
264
|
+
xdiscover(
|
|
265
|
+
timeout=timeout,
|
|
266
|
+
discover_ip_address=ip_address,
|
|
267
|
+
discover_ip_port=port,
|
|
268
|
+
)
|
|
269
|
+
) as devices:
|
|
270
|
+
async for device in devices:
|
|
271
|
+
return device
|
|
267
272
|
raise e.NetworkTimeoutError(
|
|
268
273
|
-4000,
|
|
269
274
|
"Network timeout",
|
|
@@ -273,10 +278,10 @@ async def hello(
|
|
|
273
278
|
|
|
274
279
|
async def discover(
|
|
275
280
|
timeout: float = DEFAULT_TIMEOUT,
|
|
276
|
-
local_ip_address:
|
|
281
|
+
local_ip_address: str | None = None,
|
|
277
282
|
discover_ip_address: str = DEFAULT_BCAST_ADDR,
|
|
278
283
|
discover_ip_port: int = DEFAULT_PORT,
|
|
279
|
-
) ->
|
|
284
|
+
) -> list[Device]:
|
|
280
285
|
"""Discover devices connected to the local network."""
|
|
281
286
|
return [
|
|
282
287
|
device
|
|
@@ -288,7 +293,7 @@ async def discover(
|
|
|
288
293
|
|
|
289
294
|
async def xdiscover(
|
|
290
295
|
timeout: float = DEFAULT_TIMEOUT,
|
|
291
|
-
local_ip_address:
|
|
296
|
+
local_ip_address: str | None = None,
|
|
292
297
|
discover_ip_address: str = DEFAULT_BCAST_ADDR,
|
|
293
298
|
discover_ip_port: int = DEFAULT_PORT,
|
|
294
299
|
) -> AsyncIterator[Device]:
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""Support for climate control."""
|
|
2
|
+
|
|
2
3
|
import enum
|
|
3
4
|
import struct
|
|
4
|
-
from
|
|
5
|
+
from collections.abc import Sequence
|
|
5
6
|
|
|
6
7
|
from . import exceptions as e
|
|
7
8
|
from .device import Device
|
|
@@ -33,7 +34,7 @@ class hysen(Device):
|
|
|
33
34
|
payload = self.decrypt(response[0x38:])
|
|
34
35
|
|
|
35
36
|
p_len = int.from_bytes(payload[:0x02], "little")
|
|
36
|
-
nom_crc = int.from_bytes(payload[p_len:p_len+2], "little")
|
|
37
|
+
nom_crc = int.from_bytes(payload[p_len : p_len + 2], "little")
|
|
37
38
|
real_crc = CRC16.calculate(payload[0x02:p_len])
|
|
38
39
|
|
|
39
40
|
if nom_crc != real_crc:
|
|
@@ -83,9 +84,7 @@ class hysen(Device):
|
|
|
83
84
|
data["dif"] = payload[10]
|
|
84
85
|
data["svh"] = payload[11]
|
|
85
86
|
data["svl"] = payload[12]
|
|
86
|
-
data["room_temp_adj"] = (
|
|
87
|
-
int.from_bytes(payload[13:15], "big", signed=True) / 10.0
|
|
88
|
-
)
|
|
87
|
+
data["room_temp_adj"] = int.from_bytes(payload[13:15], "big", signed=True) / 10.0
|
|
89
88
|
data["fre"] = payload[15]
|
|
90
89
|
data["poweron"] = payload[16]
|
|
91
90
|
data["unknown"] = payload[17]
|
|
@@ -127,9 +126,7 @@ class hysen(Device):
|
|
|
127
126
|
# E.g. loop_mode = 0 ("12345,67") means Saturday and Sunday (weekend schedule)
|
|
128
127
|
# loop_mode = 2 ("1234567") means every day, including Saturday and Sunday (weekday schedule)
|
|
129
128
|
# The sensor command is currently experimental
|
|
130
|
-
async def set_mode(
|
|
131
|
-
self, auto_mode: int, loop_mode: int, sensor: int = 0
|
|
132
|
-
) -> None:
|
|
129
|
+
async def set_mode(self, auto_mode: int, loop_mode: int, sensor: int = 0) -> None:
|
|
133
130
|
"""Set the mode of the device."""
|
|
134
131
|
mode_byte = ((loop_mode + 1) << 4) + auto_mode
|
|
135
132
|
await self.send_request([0x01, 0x06, 0x00, 0x02, mode_byte, sensor])
|
|
@@ -210,19 +207,7 @@ class hysen(Device):
|
|
|
210
207
|
async def set_time(self, hour: int, minute: int, second: int, day: int) -> None:
|
|
211
208
|
"""Set the time."""
|
|
212
209
|
await self.send_request(
|
|
213
|
-
[
|
|
214
|
-
0x01,
|
|
215
|
-
0x10,
|
|
216
|
-
0x00,
|
|
217
|
-
0x08,
|
|
218
|
-
0x00,
|
|
219
|
-
0x02,
|
|
220
|
-
0x04,
|
|
221
|
-
hour,
|
|
222
|
-
minute,
|
|
223
|
-
second,
|
|
224
|
-
day
|
|
225
|
-
]
|
|
210
|
+
[0x01, 0x10, 0x00, 0x08, 0x00, 0x02, 0x04, hour, minute, second, day]
|
|
226
211
|
)
|
|
227
212
|
|
|
228
213
|
# Set timer schedule
|
|
@@ -231,7 +216,7 @@ class hysen(Device):
|
|
|
231
216
|
# {'start_hour':17, 'start_minute':30, 'temp': 22 }
|
|
232
217
|
# Each one specifies the thermostat temp that will become effective at start_hour:start_minute
|
|
233
218
|
# weekend is similar but only has 2 (e.g. switch on in morning and off in afternoon)
|
|
234
|
-
async def set_schedule(self, weekday:
|
|
219
|
+
async def set_schedule(self, weekday: list[dict], weekend: list[dict]) -> None:
|
|
235
220
|
"""Set timer schedule."""
|
|
236
221
|
request = [0x01, 0x10, 0x00, 0x0A, 0x00, 0x0C, 0x18]
|
|
237
222
|
|
|
@@ -317,9 +302,7 @@ class hvac(Device):
|
|
|
317
302
|
"""Encode data for transport."""
|
|
318
303
|
packet = bytearray(10)
|
|
319
304
|
p_len = 10 + len(data)
|
|
320
|
-
struct.pack_into(
|
|
321
|
-
"<HHHHH", packet, 0, p_len, 0x00BB, 0x8006, 0, len(data)
|
|
322
|
-
)
|
|
305
|
+
struct.pack_into("<HHHHH", packet, 0, p_len, 0x00BB, 0x8006, 0, len(data))
|
|
323
306
|
packet += data
|
|
324
307
|
crc = CRC16.calculate(packet[0x02:], polynomial=0x9BE4)
|
|
325
308
|
packet += crc.to_bytes(2, "little")
|
|
@@ -330,7 +313,7 @@ class hvac(Device):
|
|
|
330
313
|
# payload[0x2:0x8] == bytes([0xbb, 0x00, 0x07, 0x00, 0x00, 0x00])
|
|
331
314
|
payload = self.decrypt(response[0x38:])
|
|
332
315
|
p_len = int.from_bytes(payload[:0x02], "little")
|
|
333
|
-
nom_crc = int.from_bytes(payload[p_len:p_len+2], "little")
|
|
316
|
+
nom_crc = int.from_bytes(payload[p_len : p_len + 2], "little")
|
|
334
317
|
real_crc = CRC16.calculate(payload[0x02:p_len], polynomial=0x9BE4)
|
|
335
318
|
|
|
336
319
|
if nom_crc != real_crc:
|
|
@@ -341,7 +324,7 @@ class hvac(Device):
|
|
|
341
324
|
)
|
|
342
325
|
|
|
343
326
|
d_len = int.from_bytes(payload[0x08:0x0A], "little")
|
|
344
|
-
return payload[0x0A:0x0A+d_len]
|
|
327
|
+
return payload[0x0A : 0x0A + d_len]
|
|
345
328
|
|
|
346
329
|
async def _send(self, command: int, data: bytes = b"") -> bytes:
|
|
347
330
|
"""Send a command to the unit."""
|