python-broadlink 1.0.0__py3-none-any.whl → 1.0.1__py3-none-any.whl
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.
- broadlink/device.py +86 -32
- broadlink/remote.py +123 -62
- {python_broadlink-1.0.0.dist-info → python_broadlink-1.0.1.dist-info}/METADATA +11 -4
- {python_broadlink-1.0.0.dist-info → python_broadlink-1.0.1.dist-info}/RECORD +7 -7
- {python_broadlink-1.0.0.dist-info → python_broadlink-1.0.1.dist-info}/WHEEL +0 -0
- {python_broadlink-1.0.0.dist-info → python_broadlink-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {python_broadlink-1.0.0.dist-info → python_broadlink-1.0.1.dist-info}/top_level.txt +0 -0
broadlink/device.py
CHANGED
|
@@ -10,6 +10,7 @@ speaks unprompted, so each device keeps a single datagram endpoint and an
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import asyncio
|
|
13
|
+
import collections
|
|
13
14
|
import random
|
|
14
15
|
import socket
|
|
15
16
|
from collections.abc import AsyncIterator
|
|
@@ -30,8 +31,17 @@ from .protocol import Datetime
|
|
|
30
31
|
HelloResponse = Tuple[int, Tuple[str, int], bytes, str, bool]
|
|
31
32
|
|
|
32
33
|
# Device error codes that mean the session key is no longer accepted and a
|
|
33
|
-
# fresh auth() will fix it. -
|
|
34
|
-
|
|
34
|
+
# fresh auth() will fix it. -2: logged out; -7: control key expired;
|
|
35
|
+
# -4012: control id error.
|
|
36
|
+
_REAUTH_CODES = {-2, -7, -4012}
|
|
37
|
+
|
|
38
|
+
# How many timed-out request counters to remember, so that a reply to one
|
|
39
|
+
# of them arriving late is recognised and dropped instead of being taken as
|
|
40
|
+
# the answer to a later request.
|
|
41
|
+
_ABANDONED_MAX = 32
|
|
42
|
+
|
|
43
|
+
_CLOSED = (None, None)
|
|
44
|
+
"""Sentinel put on the receive queue when the endpoint is closed."""
|
|
35
45
|
|
|
36
46
|
|
|
37
47
|
class _Protocol(asyncio.DatagramProtocol):
|
|
@@ -203,7 +213,10 @@ class Device:
|
|
|
203
213
|
self._lock: Optional[asyncio.Lock] = None
|
|
204
214
|
self._transport: Optional[asyncio.DatagramTransport] = None
|
|
205
215
|
self._protocol: Optional[_Protocol] = None
|
|
206
|
-
self.
|
|
216
|
+
self._endpoint_addr: Optional[Tuple[str, int]] = None
|
|
217
|
+
self._abandoned: collections.deque[int] = collections.deque(maxlen=_ABANDONED_MAX)
|
|
218
|
+
self._reauth_lock: Optional[asyncio.Lock] = None
|
|
219
|
+
self._auth_generation = 0
|
|
207
220
|
|
|
208
221
|
def __repr__(self) -> str:
|
|
209
222
|
"""Return a formal representation of the device."""
|
|
@@ -275,6 +288,7 @@ class Device:
|
|
|
275
288
|
|
|
276
289
|
self.id = int.from_bytes(payload[:0x4], "little")
|
|
277
290
|
self.update_aes(payload[0x04:0x14])
|
|
291
|
+
self._auth_generation += 1
|
|
278
292
|
return True
|
|
279
293
|
|
|
280
294
|
async def hello(self, local_ip_address=None) -> bool:
|
|
@@ -363,17 +377,30 @@ class Device:
|
|
|
363
377
|
# -------------------------------------------------------- transport
|
|
364
378
|
|
|
365
379
|
async def aclose(self) -> None:
|
|
366
|
-
"""Close the device's endpoint. It is reopened on the next call.
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
380
|
+
"""Close the device's endpoint. It is reopened on the next call.
|
|
381
|
+
|
|
382
|
+
A request in flight fails at once with ``ConnectionClosedError``
|
|
383
|
+
rather than waiting out its timeout.
|
|
384
|
+
"""
|
|
385
|
+
transport, protocol = self._transport, self._protocol
|
|
386
|
+
self._transport = None
|
|
387
|
+
self._protocol = None
|
|
388
|
+
self._endpoint_addr = None
|
|
389
|
+
if transport is not None:
|
|
390
|
+
transport.close()
|
|
391
|
+
if protocol is not None:
|
|
392
|
+
protocol.queue.put_nowait(_CLOSED) # type: ignore[arg-type]
|
|
371
393
|
|
|
372
394
|
async def _endpoint(self) -> tuple[asyncio.DatagramTransport, _Protocol]:
|
|
395
|
+
if self._transport is not None and self._endpoint_addr != self.host:
|
|
396
|
+
# The caller changed host; the connected socket points at the
|
|
397
|
+
# old address, so drop it.
|
|
398
|
+
await self.aclose()
|
|
373
399
|
if self._transport is None or self._transport.is_closing():
|
|
374
400
|
self._transport, self._protocol = await _open_endpoint(
|
|
375
401
|
remote_addr=self.host
|
|
376
402
|
)
|
|
403
|
+
self._endpoint_addr = self.host
|
|
377
404
|
return self._transport, self._protocol # type: ignore[return-value]
|
|
378
405
|
|
|
379
406
|
def _frame(self, packet_type: int, payload: bytes) -> bytes:
|
|
@@ -419,28 +446,53 @@ class Device:
|
|
|
419
446
|
return resp
|
|
420
447
|
|
|
421
448
|
async def _exchange(self, packet: bytes) -> bytes:
|
|
422
|
-
"""Send one frame and wait for
|
|
449
|
+
"""Send one frame and wait for its reply, resending on silence.
|
|
450
|
+
|
|
451
|
+
Replies carry the request's packet counter (offset 0x28), so a reply
|
|
452
|
+
is matched to the request by counter. A reply whose counter belongs
|
|
453
|
+
to a request that already timed out is dropped; one with a counter
|
|
454
|
+
this device has never sent is accepted, for firmware that may not
|
|
455
|
+
echo it.
|
|
456
|
+
"""
|
|
423
457
|
transport, protocol = await self._endpoint()
|
|
424
458
|
protocol.drain()
|
|
425
459
|
loop = asyncio.get_running_loop()
|
|
426
460
|
start = loop.time()
|
|
427
461
|
timeout = self.timeout
|
|
462
|
+
count = int.from_bytes(packet[0x28:0x2A], "little")
|
|
428
463
|
|
|
429
464
|
while True:
|
|
430
465
|
transport.sendto(packet)
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
466
|
+
resend_at = loop.time() + DEFAULT_RETRY_INTVL
|
|
467
|
+
while True:
|
|
468
|
+
now = loop.time()
|
|
469
|
+
if now - start >= timeout:
|
|
470
|
+
break
|
|
471
|
+
wait = min(resend_at, start + timeout) - now
|
|
472
|
+
try:
|
|
473
|
+
resp, _ = await asyncio.wait_for(protocol.queue.get(), max(wait, 0))
|
|
474
|
+
except asyncio.TimeoutError:
|
|
475
|
+
if loop.time() - start >= timeout:
|
|
476
|
+
break
|
|
477
|
+
if loop.time() >= resend_at:
|
|
478
|
+
break # Resend.
|
|
479
|
+
continue
|
|
480
|
+
if resp is None:
|
|
481
|
+
raise e.ConnectionClosedError(
|
|
482
|
+
-4013, "Connection closed", "The device endpoint was closed"
|
|
483
|
+
)
|
|
484
|
+
resp = self._validate(resp)
|
|
485
|
+
reply_count = int.from_bytes(resp[0x28:0x2A], "little")
|
|
486
|
+
if reply_count == count or reply_count not in self._abandoned:
|
|
487
|
+
return resp
|
|
488
|
+
# A late answer to a request we gave up on: keep waiting.
|
|
489
|
+
if loop.time() - start >= timeout:
|
|
490
|
+
self._abandoned.append(count)
|
|
491
|
+
raise e.NetworkTimeoutError(
|
|
492
|
+
-4000,
|
|
493
|
+
"Network timeout",
|
|
494
|
+
f"No response received within {timeout}s",
|
|
495
|
+
) from None
|
|
444
496
|
|
|
445
497
|
async def send_packet(
|
|
446
498
|
self, packet_type: int, payload: bytes, *, _reauth: bool = True
|
|
@@ -449,22 +501,24 @@ class Device:
|
|
|
449
501
|
|
|
450
502
|
If the device answers that the session key is no longer valid, the
|
|
451
503
|
session is re-authenticated once and the request is sent again.
|
|
504
|
+
Concurrent callers that hit the same expired key share one
|
|
505
|
+
re-authentication and each retry once.
|
|
452
506
|
"""
|
|
453
507
|
if self._lock is None:
|
|
454
508
|
self._lock = asyncio.Lock()
|
|
509
|
+
self._reauth_lock = asyncio.Lock()
|
|
510
|
+
generation = self._auth_generation
|
|
455
511
|
async with self._lock:
|
|
456
512
|
resp = await self._exchange(self._frame(packet_type, bytes(payload)))
|
|
457
513
|
|
|
458
|
-
if _reauth
|
|
514
|
+
if _reauth:
|
|
459
515
|
code = int.from_bytes(resp[0x22:0x24], "little", signed=True)
|
|
460
516
|
if code in _REAUTH_CODES:
|
|
461
|
-
self.
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
finally:
|
|
469
|
-
self._reauth_ok = True
|
|
517
|
+
async with self._reauth_lock: # type: ignore[union-attr]
|
|
518
|
+
if self._auth_generation == generation:
|
|
519
|
+
await self.auth()
|
|
520
|
+
async with self._lock:
|
|
521
|
+
resp = await self._exchange(
|
|
522
|
+
self._frame(packet_type, bytes(payload))
|
|
523
|
+
)
|
|
470
524
|
return resp
|
broadlink/remote.py
CHANGED
|
@@ -4,6 +4,7 @@ import asyncio
|
|
|
4
4
|
import enum
|
|
5
5
|
import struct
|
|
6
6
|
import time
|
|
7
|
+
import weakref
|
|
7
8
|
from dataclasses import dataclass, field
|
|
8
9
|
from typing import AsyncIterator, Awaitable, Callable, List, Optional, Tuple
|
|
9
10
|
|
|
@@ -214,7 +215,40 @@ class rmmini(Device):
|
|
|
214
215
|
# against the value it saw when it armed the device and re-arms
|
|
215
216
|
# after any send, since the device has one front end for both.
|
|
216
217
|
self._tx_generation = 0
|
|
217
|
-
|
|
218
|
+
# Weak reference to the async generator of the current capture
|
|
219
|
+
# window, if any. See _claim_window.
|
|
220
|
+
self._window: Optional[weakref.ReferenceType] = None
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def capture_active(self) -> bool:
|
|
224
|
+
"""True while a capture window is open on this device."""
|
|
225
|
+
window = self._window() if self._window is not None else None
|
|
226
|
+
return window is not None and window.ag_frame is not None
|
|
227
|
+
|
|
228
|
+
def _check_window(self) -> Optional[weakref.ReferenceType]:
|
|
229
|
+
"""Refuse a new window while another is being iterated; return the
|
|
230
|
+
reference to a previous window that the new one should close."""
|
|
231
|
+
old = self._window() if self._window is not None else None
|
|
232
|
+
if old is None or old.ag_frame is None:
|
|
233
|
+
return None
|
|
234
|
+
if old.ag_running:
|
|
235
|
+
raise e.CaptureInProgressError("A capture window is already open")
|
|
236
|
+
return self._window
|
|
237
|
+
|
|
238
|
+
async def _claim_window(self, prev: Optional[weakref.ReferenceType]) -> None:
|
|
239
|
+
"""Close a previous window whose consumer walked away from it.
|
|
240
|
+
|
|
241
|
+
A window whose consumer is still iterating it is live and a new one
|
|
242
|
+
is refused at call time (``_check_window``). One the consumer broke
|
|
243
|
+
out of without closing the generator is not live; it is closed here
|
|
244
|
+
so that it cannot block the device forever.
|
|
245
|
+
"""
|
|
246
|
+
old = prev() if prev is not None else None
|
|
247
|
+
if old is None or old.ag_frame is None:
|
|
248
|
+
return
|
|
249
|
+
if old.ag_running:
|
|
250
|
+
raise e.CaptureInProgressError("A capture window is already open")
|
|
251
|
+
await old.aclose()
|
|
218
252
|
|
|
219
253
|
async def _send(self, command: int, data: bytes = b"") -> bytes:
|
|
220
254
|
"""Send a packet to the device."""
|
|
@@ -266,10 +300,13 @@ class rmmini(Device):
|
|
|
266
300
|
and after every ``send_data`` on the same device. Closing the
|
|
267
301
|
generator sends nothing further; the device times out by itself.
|
|
268
302
|
Use ``contextlib.aclosing`` (or iterate to the end) so the window is
|
|
269
|
-
released promptly. Only one capture window can be open per device
|
|
270
|
-
|
|
303
|
+
released promptly. Only one capture window can be open per device:
|
|
304
|
+
opening one while another is being iterated raises
|
|
305
|
+
``CaptureInProgressError``, and opening one after breaking out of
|
|
306
|
+
another without closing it closes the old one.
|
|
271
307
|
"""
|
|
272
|
-
|
|
308
|
+
prev = self._check_window()
|
|
309
|
+
gen = self._capture_loop(
|
|
273
310
|
self.enter_learning,
|
|
274
311
|
window,
|
|
275
312
|
stop_after_first,
|
|
@@ -277,7 +314,10 @@ class rmmini(Device):
|
|
|
277
314
|
rearm_interval,
|
|
278
315
|
SignalKind.IR,
|
|
279
316
|
None,
|
|
317
|
+
prev=prev,
|
|
280
318
|
)
|
|
319
|
+
self._window = weakref.ref(gen)
|
|
320
|
+
return gen
|
|
281
321
|
|
|
282
322
|
async def _capture_loop(
|
|
283
323
|
self,
|
|
@@ -288,58 +328,60 @@ class rmmini(Device):
|
|
|
288
328
|
rearm_interval: float,
|
|
289
329
|
kind: SignalKind,
|
|
290
330
|
frequency_mhz: Optional[float],
|
|
331
|
+
*,
|
|
332
|
+
prev: Optional[weakref.ReferenceType] = None,
|
|
333
|
+
claim: bool = True,
|
|
291
334
|
) -> AsyncIterator[CapturedSignal]:
|
|
292
335
|
if window < 0:
|
|
293
336
|
raise ValueError("window must be 0 (open-ended) or positive")
|
|
294
337
|
if poll_interval <= 0 or rearm_interval <= 0:
|
|
295
338
|
raise ValueError("poll_interval and rearm_interval must be positive")
|
|
296
|
-
if
|
|
297
|
-
|
|
339
|
+
if claim:
|
|
340
|
+
await self._claim_window(prev)
|
|
298
341
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
342
|
+
loop = asyncio.get_running_loop()
|
|
343
|
+
deadline = loop.time() + window if window else None
|
|
344
|
+
timeouts = 0
|
|
345
|
+
|
|
346
|
+
await arm()
|
|
347
|
+
armed_at = loop.time()
|
|
348
|
+
generation = self._tx_generation
|
|
304
349
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
350
|
+
while True:
|
|
351
|
+
now = loop.time()
|
|
352
|
+
if deadline is not None and now >= deadline:
|
|
353
|
+
return
|
|
354
|
+
delay = poll_interval
|
|
355
|
+
if deadline is not None:
|
|
356
|
+
delay = min(delay, deadline - now)
|
|
357
|
+
await asyncio.sleep(delay)
|
|
308
358
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
359
|
+
try:
|
|
360
|
+
data = await self.check_data()
|
|
361
|
+
except (e.StorageError, e.ReadError):
|
|
362
|
+
# "Nothing yet": -5 on the RM4 Pro, -10 on some older
|
|
363
|
+
# firmware (upstream's CLI and Home Assistant tolerate
|
|
364
|
+
# both).
|
|
365
|
+
data = b""
|
|
366
|
+
except e.NetworkTimeoutError:
|
|
367
|
+
timeouts += 1
|
|
368
|
+
if timeouts >= 3:
|
|
369
|
+
raise
|
|
370
|
+
generation = -1 # Re-arm; the device's state is unknown.
|
|
371
|
+
continue
|
|
372
|
+
timeouts = 0
|
|
373
|
+
|
|
374
|
+
if data:
|
|
375
|
+
yield CapturedSignal.from_packet(data, frequency_mhz, kind=kind)
|
|
376
|
+
if stop_after_first:
|
|
312
377
|
return
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
except e.StorageError:
|
|
321
|
-
data = b"" # The device's answer for "nothing yet".
|
|
322
|
-
except e.NetworkTimeoutError:
|
|
323
|
-
timeouts += 1
|
|
324
|
-
if timeouts >= 3:
|
|
325
|
-
raise
|
|
326
|
-
generation = -1 # Re-arm; the device's state is unknown.
|
|
327
|
-
continue
|
|
328
|
-
timeouts = 0
|
|
329
|
-
|
|
330
|
-
if data:
|
|
331
|
-
yield CapturedSignal.from_packet(data, frequency_mhz, kind=kind)
|
|
332
|
-
if stop_after_first:
|
|
333
|
-
return
|
|
334
|
-
generation = -1 # One code per session: re-arm.
|
|
335
|
-
|
|
336
|
-
now = loop.time()
|
|
337
|
-
if generation != self._tx_generation or now - armed_at >= rearm_interval:
|
|
338
|
-
await arm()
|
|
339
|
-
armed_at = loop.time()
|
|
340
|
-
generation = self._tx_generation
|
|
341
|
-
finally:
|
|
342
|
-
self._capture_open = False
|
|
378
|
+
generation = -1 # One code per session: re-arm.
|
|
379
|
+
|
|
380
|
+
now = loop.time()
|
|
381
|
+
if generation != self._tx_generation or now - armed_at >= rearm_interval:
|
|
382
|
+
await arm()
|
|
383
|
+
armed_at = loop.time()
|
|
384
|
+
generation = self._tx_generation
|
|
343
385
|
|
|
344
386
|
|
|
345
387
|
class rmpro(rmmini):
|
|
@@ -369,7 +411,7 @@ class rmpro(rmmini):
|
|
|
369
411
|
"""Cancel sweep frequency."""
|
|
370
412
|
await self._send(0x1E)
|
|
371
413
|
|
|
372
|
-
|
|
414
|
+
def capture_rf(
|
|
373
415
|
self,
|
|
374
416
|
window: float = 30.0,
|
|
375
417
|
*,
|
|
@@ -389,24 +431,38 @@ class rmpro(rmmini):
|
|
|
389
431
|
|
|
390
432
|
The window, polling, re-arm and stop-after-first semantics are those
|
|
391
433
|
of ``capture``; the sweep counts against the same ``window``. A
|
|
392
|
-
``send_data`` during the sweep restarts it.
|
|
393
|
-
|
|
394
|
-
|
|
434
|
+
``send_data`` during the sweep restarts it. Closing the generator
|
|
435
|
+
during a sweep sends nothing; the device ends the sweep on its own.
|
|
436
|
+
Each ``CapturedSignal`` carries the carrier in ``frequency_mhz``,
|
|
437
|
+
which the packet itself does not record.
|
|
395
438
|
"""
|
|
396
|
-
|
|
397
|
-
|
|
439
|
+
prev = self._check_window()
|
|
440
|
+
gen = self._capture_rf_loop(
|
|
441
|
+
window, frequency, stop_after_first, poll_interval, rearm_interval,
|
|
442
|
+
prev=prev,
|
|
443
|
+
)
|
|
444
|
+
self._window = weakref.ref(gen)
|
|
445
|
+
return gen
|
|
446
|
+
|
|
447
|
+
async def _capture_rf_loop(
|
|
448
|
+
self,
|
|
449
|
+
window: float,
|
|
450
|
+
frequency: Optional[float],
|
|
451
|
+
stop_after_first: bool,
|
|
452
|
+
poll_interval: float,
|
|
453
|
+
rearm_interval: float,
|
|
454
|
+
*,
|
|
455
|
+
prev: Optional[weakref.ReferenceType],
|
|
456
|
+
) -> AsyncIterator[CapturedSignal]:
|
|
398
457
|
if window < 0 or poll_interval <= 0:
|
|
399
458
|
raise ValueError("window must be 0 or positive, poll_interval positive")
|
|
459
|
+
await self._claim_window(prev)
|
|
400
460
|
|
|
401
461
|
loop = asyncio.get_running_loop()
|
|
402
462
|
deadline = loop.time() + window if window else None
|
|
403
463
|
|
|
404
464
|
if frequency is None:
|
|
405
|
-
self.
|
|
406
|
-
try:
|
|
407
|
-
frequency = await self._sweep(deadline, poll_interval)
|
|
408
|
-
finally:
|
|
409
|
-
self._capture_open = False
|
|
465
|
+
frequency = await self._sweep(deadline, poll_interval)
|
|
410
466
|
if frequency is None:
|
|
411
467
|
return
|
|
412
468
|
if deadline is not None:
|
|
@@ -418,10 +474,15 @@ class rmpro(rmmini):
|
|
|
418
474
|
await self.find_rf_packet(frequency)
|
|
419
475
|
|
|
420
476
|
kind = SignalKind.RF_315 if frequency < 400 else SignalKind.RF_433
|
|
421
|
-
|
|
422
|
-
arm, window, stop_after_first, poll_interval, rearm_interval, kind, frequency
|
|
423
|
-
|
|
424
|
-
|
|
477
|
+
inner = self._capture_loop(
|
|
478
|
+
arm, window, stop_after_first, poll_interval, rearm_interval, kind, frequency,
|
|
479
|
+
claim=False,
|
|
480
|
+
)
|
|
481
|
+
try:
|
|
482
|
+
async for signal in inner:
|
|
483
|
+
yield signal
|
|
484
|
+
finally:
|
|
485
|
+
await inner.aclose()
|
|
425
486
|
|
|
426
487
|
async def _sweep(
|
|
427
488
|
self, deadline: Optional[float], poll_interval: float
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-broadlink
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Python API for controlling Broadlink devices
|
|
5
5
|
Author: DAB-LABS
|
|
6
6
|
Author-email: Matthew Garrett <mjg59@srcf.ucam.org>
|
|
@@ -91,9 +91,16 @@ Use pip3 to install the latest version of this module.
|
|
|
91
91
|
pip3 install python-broadlink
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
Both this distribution and the original `broadlink` install a package named
|
|
95
|
+
`broadlink`, so only one can be present in an environment at a time. Pip
|
|
96
|
+
does not warn about this: installing one on top of the other appears to
|
|
97
|
+
succeed, and whichever was installed last is the one that `import broadlink`
|
|
98
|
+
finds. If both were installed, uninstall both (`pip3 uninstall broadlink
|
|
99
|
+
python-broadlink`) and reinstall this one, since `pip3 uninstall broadlink`
|
|
100
|
+
alone removes the shared files and leaves `python-broadlink` registered but
|
|
101
|
+
unimportable. This matters most where another package pins `broadlink`:
|
|
102
|
+
installing it into the same environment silently replaces this async
|
|
103
|
+
library with the original synchronous one.
|
|
97
104
|
|
|
98
105
|
## Basic functions
|
|
99
106
|
|
|
@@ -3,17 +3,17 @@ broadlink/alarm.py,sha256=ZsPKG4xibM9mxuuiWJcv_XPOOwFogGMfuP-3qOvyuA0,1259
|
|
|
3
3
|
broadlink/climate.py,sha256=YJUi6oVrvY941R5AqToADObZbjuQd7NZ7E2u0UbVrMM,15826
|
|
4
4
|
broadlink/const.py,sha256=MDzkcoaWN_4zXPTiG2DnYZgr6w97zX7tsnDfImYgA-Q,119
|
|
5
5
|
broadlink/cover.py,sha256=bnUmQYUucxOogmYjmk57vwDsIvMjDIL_WKvMb-IdjJc,5438
|
|
6
|
-
broadlink/device.py,sha256=
|
|
6
|
+
broadlink/device.py,sha256=_3Ia-SXjeywcCi99-81zPhpotOIj0r-IENOr0sM1fLU,19248
|
|
7
7
|
broadlink/exceptions.py,sha256=0uooFfEd34id60fddcsXvU7uQ_4lFt07iDYDE5NkCSA,4756
|
|
8
8
|
broadlink/helpers.py,sha256=Kpm7GUSXmwhNMJGFWfVNJ_1idcO1D-k5rgzHbO8vDlM,1296
|
|
9
9
|
broadlink/hub.py,sha256=unQ45HIzIUmixqXrb7RvuWO5y7wvGAHFEPfR8OVWsjA,2929
|
|
10
10
|
broadlink/light.py,sha256=2t7Jumm4DHP81RzeLFUQQDhXK5VcvgmH3osrkufCaIs,7782
|
|
11
11
|
broadlink/protocol.py,sha256=9pNT6HrsCzHyeqpzhxKOkR58GjHhfVz8QdyefceI4tE,1847
|
|
12
|
-
broadlink/remote.py,sha256=
|
|
12
|
+
broadlink/remote.py,sha256=pqs7NWE8m21U9V3F44KA-Gdk4Ai5oj5nLLj81WS9_xo,20405
|
|
13
13
|
broadlink/sensor.py,sha256=K76lAF-addjaq3-l6pl-buaqZelRd62sDE21cEz-lnI,2691
|
|
14
14
|
broadlink/switch.py,sha256=WzCi-UbWSlIEytGc9E0-oAje2Z8E1453eUYmFq_hjf8,15060
|
|
15
|
-
python_broadlink-1.0.
|
|
16
|
-
python_broadlink-1.0.
|
|
17
|
-
python_broadlink-1.0.
|
|
18
|
-
python_broadlink-1.0.
|
|
19
|
-
python_broadlink-1.0.
|
|
15
|
+
python_broadlink-1.0.1.dist-info/licenses/LICENSE,sha256=h0N_LWq34pY0W-Ky3ayO6Re5ejG0XvutPlW-GL8eG5I,1111
|
|
16
|
+
python_broadlink-1.0.1.dist-info/METADATA,sha256=1KVEvHEbXlFG_dZYBLf6cMl2RihqDyo841FzjC_xog8,11908
|
|
17
|
+
python_broadlink-1.0.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
18
|
+
python_broadlink-1.0.1.dist-info/top_level.txt,sha256=_Qx7d8oNTOLeDkAN-HHDoq1FlRNlsiaMZlyMWgHXAVc,10
|
|
19
|
+
python_broadlink-1.0.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|