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 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, self._queueCallback
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
- while True:
237
- handle, data = await self._callbackQueue.get()
238
-
239
- # Try to loose any races here.
240
- # Otherwise a state change caused by the last packet might not have been handled yet
241
- await asyncio.sleep(0.001)
242
- await self._activityLock.acquire()
243
- try:
244
- self._callbackMulitplexer(handle, data)
245
- finally:
246
- self._callbackQueue.task_done()
247
- self._activityLock.release()
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:], self._inPacketCount, raw_encrypted_packet
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
- self._callbackTask = None
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
- def __repr__(self) -> str:
273
- 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})"
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.7.dev13
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.22
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=2rcwBQ6fuFsM9Zlp8tO_relirwMi5lJq7NDmsQ72HFo,30249
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=M-Q8-Xd3qjJSUEvsFtic8E4xDc_gtWYakbTGyoIA-P8,16377
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.7.dev13.dist-info/licenses/LICENSE,sha256=TAIIitFxpxEDi6Iju7foW4TDQmWvC-IhLVLhl67jKmQ,11341
15
- casambi_bt_revamped-0.3.7.dev13.dist-info/METADATA,sha256=-xP_7qA_GzrD9o_QkzHoKIZmPCF963yjYqh92VmVkCA,3049
16
- casambi_bt_revamped-0.3.7.dev13.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
- casambi_bt_revamped-0.3.7.dev13.dist-info/top_level.txt,sha256=uNbqLjtecFosoFzpGAC89-5icikWODKI8rOjbi8v_sA,10
18
- casambi_bt_revamped-0.3.7.dev13.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5