casambi-bt-revamped 0.3.7.dev13__py3-none-any.whl → 0.3.9__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.
- CasambiBt/_client.py +51 -15
- CasambiBt/_unit.py +20 -2
- {casambi_bt_revamped-0.3.7.dev13.dist-info → casambi_bt_revamped-0.3.9.dist-info}/METADATA +2 -2
- {casambi_bt_revamped-0.3.7.dev13.dist-info → casambi_bt_revamped-0.3.9.dist-info}/RECORD +7 -7
- {casambi_bt_revamped-0.3.7.dev13.dist-info → casambi_bt_revamped-0.3.9.dist-info}/WHEEL +1 -1
- {casambi_bt_revamped-0.3.7.dev13.dist-info → casambi_bt_revamped-0.3.9.dist-info}/licenses/LICENSE +0 -0
- {casambi_bt_revamped-0.3.7.dev13.dist-info → casambi_bt_revamped-0.3.9.dist-info}/top_level.txt +0 -0
CasambiBt/_client.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import inspect
|
|
2
3
|
import logging
|
|
3
4
|
import struct
|
|
4
5
|
from binascii import b2a_hex as b2a
|
|
@@ -183,8 +184,15 @@ class CasambiClient:
|
|
|
183
184
|
|
|
184
185
|
# Device will initiate key exchange, so listen for that
|
|
185
186
|
self._logger.debug("Starting notify")
|
|
187
|
+
notify_kwargs: dict[str, Any] = {}
|
|
188
|
+
notify_params = inspect.signature(self._gattClient.start_notify).parameters
|
|
189
|
+
if "bluez" in notify_params:
|
|
190
|
+
notify_kwargs["bluez"] = {"use_start_notify": True}
|
|
191
|
+
|
|
186
192
|
await self._gattClient.start_notify(
|
|
187
|
-
CASA_AUTH_CHAR_UUID,
|
|
193
|
+
CASA_AUTH_CHAR_UUID,
|
|
194
|
+
self._queueCallback,
|
|
195
|
+
**notify_kwargs,
|
|
188
196
|
)
|
|
189
197
|
finally:
|
|
190
198
|
self._activityLock.release()
|
|
@@ -233,18 +241,23 @@ class CasambiClient:
|
|
|
233
241
|
self._callbackQueue.put_nowait((handle, data))
|
|
234
242
|
|
|
235
243
|
async def _processCallbacks(self) -> None:
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
244
|
+
try:
|
|
245
|
+
while True:
|
|
246
|
+
handle, data = await self._callbackQueue.get()
|
|
247
|
+
|
|
248
|
+
# Try to loose any races here.
|
|
249
|
+
# Otherwise a state change caused by the last packet might not have been handled yet
|
|
250
|
+
await asyncio.sleep(0.001)
|
|
251
|
+
await self._activityLock.acquire()
|
|
252
|
+
try:
|
|
253
|
+
self._callbackMulitplexer(handle, data)
|
|
254
|
+
finally:
|
|
255
|
+
self._callbackQueue.task_done()
|
|
256
|
+
self._activityLock.release()
|
|
257
|
+
except asyncio.CancelledError:
|
|
258
|
+
# Task cancelled during shutdown; log at debug and exit cleanly.
|
|
259
|
+
self._logger.debug("Callback processing task cancelled during shutdown.")
|
|
260
|
+
raise
|
|
248
261
|
|
|
249
262
|
def _callbackMulitplexer(
|
|
250
263
|
self, handle: BleakGATTCharacteristic, data: bytes
|
|
@@ -418,6 +431,13 @@ class CasambiClient:
|
|
|
418
431
|
# Store raw encrypted packet for reference
|
|
419
432
|
raw_encrypted_packet = data[:]
|
|
420
433
|
|
|
434
|
+
# Extract the device-provided 4-byte little-endian counter from the
|
|
435
|
+
# encrypted header. This is the true per-session packet sequence.
|
|
436
|
+
try:
|
|
437
|
+
device_sequence = int.from_bytes(data[:4], byteorder="little", signed=False)
|
|
438
|
+
except Exception:
|
|
439
|
+
device_sequence = None
|
|
440
|
+
|
|
421
441
|
try:
|
|
422
442
|
decrypted_data = self._encryptor.decryptAndVerify(
|
|
423
443
|
data, data[:4] + self._nonce[4:]
|
|
@@ -433,8 +453,11 @@ class CasambiClient:
|
|
|
433
453
|
if packetType == IncommingPacketType.UnitState:
|
|
434
454
|
self._parseUnitStates(decrypted_data[1:])
|
|
435
455
|
elif packetType == IncommingPacketType.SwitchEvent:
|
|
456
|
+
# Pass the device sequence as the packet sequence for consumers,
|
|
457
|
+
# and still include the raw encrypted packet for diagnostics.
|
|
458
|
+
seq_for_consumer = device_sequence if device_sequence is not None else self._inPacketCount
|
|
436
459
|
self._parseSwitchEvent(
|
|
437
|
-
decrypted_data[1:],
|
|
460
|
+
decrypted_data[1:], seq_for_consumer, raw_encrypted_packet
|
|
438
461
|
)
|
|
439
462
|
elif packetType == IncommingPacketType.NetworkConfig:
|
|
440
463
|
# We don't care about the config the network thinks it has.
|
|
@@ -733,7 +756,11 @@ class CasambiClient:
|
|
|
733
756
|
"event": event_string,
|
|
734
757
|
"flags": flags,
|
|
735
758
|
"extra_data": extra_data,
|
|
759
|
+
# packet_sequence is the device-provided sequence number when available
|
|
760
|
+
# (true 32-bit counter from the BLE header), otherwise the local arrival index.
|
|
736
761
|
"packet_sequence": packet_seq,
|
|
762
|
+
# Include the local arrival index for debugging and correlation.
|
|
763
|
+
"arrival_sequence": self._inPacketCount,
|
|
737
764
|
"raw_packet": b2a(raw_packet) if raw_packet else None,
|
|
738
765
|
"decrypted_data": b2a(full_data),
|
|
739
766
|
"message_position": start_pos,
|
|
@@ -745,8 +772,17 @@ class CasambiClient:
|
|
|
745
772
|
self._logger.info("Disconnecting...")
|
|
746
773
|
|
|
747
774
|
if self._callbackTask is not None:
|
|
775
|
+
# Cancel and await the background callback task to avoid
|
|
776
|
+
# 'Task was destroyed but it is pending' warnings.
|
|
748
777
|
self._callbackTask.cancel()
|
|
749
|
-
|
|
778
|
+
try:
|
|
779
|
+
await self._callbackTask
|
|
780
|
+
except asyncio.CancelledError:
|
|
781
|
+
pass
|
|
782
|
+
except Exception:
|
|
783
|
+
self._logger.debug("Callback task finished with exception during disconnect.", exc_info=True)
|
|
784
|
+
finally:
|
|
785
|
+
self._callbackTask = None
|
|
750
786
|
|
|
751
787
|
if self._gattClient is not None and self._gattClient.is_connected:
|
|
752
788
|
try:
|
CasambiBt/_unit.py
CHANGED
|
@@ -111,6 +111,7 @@ class UnitState:
|
|
|
111
111
|
self._colorsource: ColorSource | None = None
|
|
112
112
|
self._xy: tuple[float, float] | None = None
|
|
113
113
|
self._slider: int | None = None
|
|
114
|
+
self._onoff: bool | None = None
|
|
114
115
|
|
|
115
116
|
def _check_range(
|
|
116
117
|
self, value: int | float, min: int | float, max: int | float
|
|
@@ -269,9 +270,20 @@ class UnitState:
|
|
|
269
270
|
def slider(self) -> None:
|
|
270
271
|
self.slider = None
|
|
271
272
|
|
|
272
|
-
|
|
273
|
-
|
|
273
|
+
@property
|
|
274
|
+
def onoff(self) -> bool | None:
|
|
275
|
+
return self._onoff
|
|
276
|
+
|
|
277
|
+
@onoff.setter
|
|
278
|
+
def onoff(self, value: bool) -> None:
|
|
279
|
+
self._onoff = value
|
|
274
280
|
|
|
281
|
+
@onoff.deleter
|
|
282
|
+
def onoff(self) -> None:
|
|
283
|
+
self._onoff = None
|
|
284
|
+
|
|
285
|
+
def __repr__(self) -> str:
|
|
286
|
+
return f"UnitState(dimmer={self.dimmer}, vertical={self._vertical}, rgb={self.rgb.__repr__()}, white={self.white}, temperature={self.temperature}, colorsource={self.colorsource}, xy={self.xy}, slider={self.slider}, onoff={self.onoff})"
|
|
275
287
|
|
|
276
288
|
# TODO: Make unit immutable (refactor state, on, online out of it)
|
|
277
289
|
@dataclass(init=True, repr=True)
|
|
@@ -308,6 +320,8 @@ class Unit:
|
|
|
308
320
|
@property
|
|
309
321
|
def is_on(self) -> bool:
|
|
310
322
|
"""Determine whether the unit is turned on."""
|
|
323
|
+
if self.unitType.get_control(UnitControlType.ONOFF) and self._state:
|
|
324
|
+
return self._on and self._state.onoff is True
|
|
311
325
|
if self.unitType.get_control(UnitControlType.DIMMER) and self._state:
|
|
312
326
|
return (
|
|
313
327
|
self._on and self._state.dimmer is not None and self._state.dimmer > 0
|
|
@@ -385,6 +399,8 @@ class Unit:
|
|
|
385
399
|
elif c.type == UnitControlType.SLIDER and state.slider is not None:
|
|
386
400
|
scale = UnitState.SLIDER_RESOLUTION - c.length
|
|
387
401
|
scaledValue = state.slider >> scale
|
|
402
|
+
elif c.type == UnitControlType.ONOFF and state.onoff is not None:
|
|
403
|
+
scaledValue = 1 if state.onoff else 0
|
|
388
404
|
|
|
389
405
|
# Use default if unsupported type or unset value in state
|
|
390
406
|
else:
|
|
@@ -477,6 +493,8 @@ class Unit:
|
|
|
477
493
|
elif c.type == UnitControlType.SLIDER:
|
|
478
494
|
scale = UnitState.SLIDER_RESOLUTION - c.length
|
|
479
495
|
self._state.slider = cInt << scale
|
|
496
|
+
elif c.type == UnitControlType.ONOFF:
|
|
497
|
+
self._state.onoff = cInt != 0
|
|
480
498
|
elif c.type == UnitControlType.UNKOWN:
|
|
481
499
|
# Might be useful for implementing more state types
|
|
482
500
|
_LOGGER.debug(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: casambi-bt-revamped
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.9
|
|
4
4
|
Summary: Enhanced Casambi Bluetooth client library with switch event support
|
|
5
5
|
Home-page: https://github.com/rankjie/casambi-bt
|
|
6
6
|
Author: rankjie
|
|
@@ -12,7 +12,7 @@ Classifier: Development Status :: 4 - Beta
|
|
|
12
12
|
Requires-Python: >=3.11
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
|
-
Requires-Dist: bleak>=0
|
|
15
|
+
Requires-Dist: bleak>=2.1.0
|
|
16
16
|
Requires-Dist: cryptography>=40.0.0
|
|
17
17
|
Requires-Dist: httpx>=0.25
|
|
18
18
|
Requires-Dist: bleak_retry_connector>=3.6.0
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
CasambiBt/__init__.py,sha256=TW445xSu5PV3TyMjJfwaA1JoWvQQ8LXhZgGdDTfWf3s,302
|
|
2
2
|
CasambiBt/_cache.py,sha256=KZ2xbiHAHXUPa8Gw_75Nw9NL4QSY_sTWHbyYXYUDaB0,3865
|
|
3
3
|
CasambiBt/_casambi.py,sha256=AfyuzEU2ylJOGLmZ87Qft-aNXI_JK8Ng9Tfk4fWYOwo,34345
|
|
4
|
-
CasambiBt/_client.py,sha256=
|
|
4
|
+
CasambiBt/_client.py,sha256=cmRXPxURJpp8xQM-FJe2NBMtfFFC7NnWExTaN65LQ5c,32142
|
|
5
5
|
CasambiBt/_constants.py,sha256=_AxkG7Btxl4VeS6mO7GJW5Kc9dFs3s9sDmtJ83ZEKNw,359
|
|
6
6
|
CasambiBt/_discover.py,sha256=H7HpiFYIy9ELvmPXXd_ck-5O5invJf15dDIRk-vO5IE,1696
|
|
7
7
|
CasambiBt/_encryption.py,sha256=CLcoOOrggQqhJbnr_emBnEnkizpWDvb_0yFnitq4_FM,3831
|
|
8
8
|
CasambiBt/_keystore.py,sha256=Jdiq0zMPDmhfpheSojKY6sTUpmVrvX_qOyO7yCYd3kw,2788
|
|
9
9
|
CasambiBt/_network.py,sha256=Gh0n3FEcOUHUMuBXALwcb3tws-AofpYLegKIquqtZl4,14665
|
|
10
10
|
CasambiBt/_operation.py,sha256=Q5UccsrtNp_B_wWqwH_3eLFW_yF6A55FMmfUKDk2WrI,1059
|
|
11
|
-
CasambiBt/_unit.py,sha256=
|
|
11
|
+
CasambiBt/_unit.py,sha256=KR_dvVhCH8WIPGJgZYHyAPVA6ru0KmMOL5NgkxYHIUQ,17042
|
|
12
12
|
CasambiBt/errors.py,sha256=0JgDjaKlAKDes0poWzA8nrTUYQ8qdNfBb8dfaqqzCRA,1664
|
|
13
13
|
CasambiBt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
-
casambi_bt_revamped-0.3.
|
|
15
|
-
casambi_bt_revamped-0.3.
|
|
16
|
-
casambi_bt_revamped-0.3.
|
|
17
|
-
casambi_bt_revamped-0.3.
|
|
18
|
-
casambi_bt_revamped-0.3.
|
|
14
|
+
casambi_bt_revamped-0.3.9.dist-info/licenses/LICENSE,sha256=TAIIitFxpxEDi6Iju7foW4TDQmWvC-IhLVLhl67jKmQ,11341
|
|
15
|
+
casambi_bt_revamped-0.3.9.dist-info/METADATA,sha256=gZzMScOl8A22GnaSMjFoAwu1Q2kJWwF49GQXcQFd0pk,3044
|
|
16
|
+
casambi_bt_revamped-0.3.9.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
17
|
+
casambi_bt_revamped-0.3.9.dist-info/top_level.txt,sha256=uNbqLjtecFosoFzpGAC89-5icikWODKI8rOjbi8v_sA,10
|
|
18
|
+
casambi_bt_revamped-0.3.9.dist-info/RECORD,,
|
{casambi_bt_revamped-0.3.7.dev13.dist-info → casambi_bt_revamped-0.3.9.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{casambi_bt_revamped-0.3.7.dev13.dist-info → casambi_bt_revamped-0.3.9.dist-info}/top_level.txt
RENAMED
|
File without changes
|