casambi-bt-revamped 0.3.12.dev8__py3-none-any.whl → 0.3.12.dev9__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 +180 -17
- CasambiBt/_constants.py +9 -0
- CasambiBt/_network.py +26 -19
- CasambiBt/_version.py +1 -1
- {casambi_bt_revamped-0.3.12.dev8.dist-info → casambi_bt_revamped-0.3.12.dev9.dist-info}/METADATA +1 -1
- {casambi_bt_revamped-0.3.12.dev8.dist-info → casambi_bt_revamped-0.3.12.dev9.dist-info}/RECORD +9 -9
- {casambi_bt_revamped-0.3.12.dev8.dist-info → casambi_bt_revamped-0.3.12.dev9.dist-info}/WHEEL +0 -0
- {casambi_bt_revamped-0.3.12.dev8.dist-info → casambi_bt_revamped-0.3.12.dev9.dist-info}/licenses/LICENSE +0 -0
- {casambi_bt_revamped-0.3.12.dev8.dist-info → casambi_bt_revamped-0.3.12.dev9.dist-info}/top_level.txt +0 -0
CasambiBt/_client.py
CHANGED
|
@@ -25,7 +25,13 @@ from cryptography.exceptions import InvalidSignature
|
|
|
25
25
|
from cryptography.hazmat.primitives.asymmetric import ec
|
|
26
26
|
|
|
27
27
|
from ._constants import CASA_AUTH_CHAR_UUID, ConnectionState
|
|
28
|
-
from ._constants import
|
|
28
|
+
from ._constants import (
|
|
29
|
+
CASA_CLASSIC_CA53_CHAR_UUID,
|
|
30
|
+
CASA_CLASSIC_CONFORMANT_CA51_CHAR_UUID,
|
|
31
|
+
CASA_CLASSIC_CONFORMANT_CA53_CHAR_UUID,
|
|
32
|
+
CASA_CLASSIC_DATA_CHAR_UUID,
|
|
33
|
+
CASA_CLASSIC_HASH_CHAR_UUID,
|
|
34
|
+
)
|
|
29
35
|
from ._classic_crypto import classic_cmac_prefix
|
|
30
36
|
from ._encryption import Encryptor
|
|
31
37
|
from ._network import Network
|
|
@@ -135,6 +141,12 @@ class CasambiClient:
|
|
|
135
141
|
# - "legacy": [sig][payload]
|
|
136
142
|
# Ground truth: casambi-android `t1.P.n(...)` and `t1.P.o(...)`.
|
|
137
143
|
self._classicHeaderMode: str | None = None # "conformant" | "legacy"
|
|
144
|
+
# Classic transport diagnostics / channel selection.
|
|
145
|
+
self._classicTxCharUuid: str | None = None
|
|
146
|
+
self._classicNotifyCharUuids: set[str] = set()
|
|
147
|
+
self._classicHashSource: str | None = None # "ca51" | "ca52_0001" | None
|
|
148
|
+
self._classicFirstRxTs: float | None = None
|
|
149
|
+
self._classicNoRxTask: asyncio.Task[None] | None = None
|
|
138
150
|
|
|
139
151
|
# Rate limit WARNING logs (especially Classic RX) to keep HA usable.
|
|
140
152
|
self._logLimiter = _LogBurstLimiter()
|
|
@@ -186,6 +198,23 @@ class CasambiClient:
|
|
|
186
198
|
self._outPacketCount = 2
|
|
187
199
|
self._inPacketCount = 1
|
|
188
200
|
|
|
201
|
+
# Reset protocol-specific state (important for reconnects).
|
|
202
|
+
self._protocolMode = None
|
|
203
|
+
self._dataCharUuid = None
|
|
204
|
+
self._deviceProtocolVersion = None
|
|
205
|
+
|
|
206
|
+
self._classicConnHash8 = None
|
|
207
|
+
self._classicTxSeq = 0
|
|
208
|
+
self._classicCmdDiv = 0
|
|
209
|
+
self._classicHeaderMode = None
|
|
210
|
+
self._classicTxCharUuid = None
|
|
211
|
+
self._classicNotifyCharUuids.clear()
|
|
212
|
+
self._classicHashSource = None
|
|
213
|
+
self._classicFirstRxTs = None
|
|
214
|
+
if self._classicNoRxTask is not None:
|
|
215
|
+
self._classicNoRxTask.cancel()
|
|
216
|
+
self._classicNoRxTask = None
|
|
217
|
+
|
|
189
218
|
# Reset callback queue
|
|
190
219
|
self._callbackQueue = asyncio.Queue()
|
|
191
220
|
self._callbackTask = asyncio.create_task(self._processCallbacks())
|
|
@@ -262,28 +291,43 @@ class CasambiClient:
|
|
|
262
291
|
cloud_protocol = getattr(self._network, "protocolVersion", None)
|
|
263
292
|
ca51_prefix: bytes | None = None
|
|
264
293
|
ca51_err: str | None = None
|
|
294
|
+
ca52_notify_err: str | None = None
|
|
295
|
+
ca53_notify_err: str | None = None
|
|
265
296
|
auth_prefix: bytes | None = None
|
|
266
297
|
auth_err: str | None = None
|
|
298
|
+
c0002_prefix: bytes | None = None
|
|
299
|
+
c0002_err: str | None = None
|
|
300
|
+
c0003_notify_err: str | None = None
|
|
267
301
|
device_nodeinfo_protocol: int | None = None
|
|
268
302
|
|
|
269
303
|
def _log_probe_summary(mode: str, *, classic_variant: str | None = None) -> None:
|
|
270
304
|
# One stable, high-signal line for testers.
|
|
271
305
|
self._logger.warning(
|
|
272
306
|
"[CASAMBI_PROTOCOL_PROBE] address=%s mode=%s cloud_protocol=%s nodeinfo_b1=%s data_uuid=%s "
|
|
273
|
-
"classic_variant=%s
|
|
274
|
-
"
|
|
307
|
+
"classic_variant=%s hash_source=%s classic_tx_uuid=%s classic_notify_uuids=%s "
|
|
308
|
+
"ca51_hash8_present=%s conn_hash8_ready=%s "
|
|
309
|
+
"auth_read_prefix=%s ca51_read_prefix=%s ca51_read_error=%s auth_read_error=%s "
|
|
310
|
+
"ca52_notify_error=%s ca53_notify_error=%s c0002_read_prefix=%s c0002_read_error=%s c0003_notify_error=%s",
|
|
275
311
|
self.address,
|
|
276
312
|
mode,
|
|
277
313
|
cloud_protocol,
|
|
278
314
|
device_nodeinfo_protocol,
|
|
279
315
|
self._dataCharUuid,
|
|
280
316
|
classic_variant,
|
|
317
|
+
self._classicHashSource,
|
|
318
|
+
self._classicTxCharUuid,
|
|
319
|
+
sorted(self._classicNotifyCharUuids) if self._classicNotifyCharUuids else None,
|
|
281
320
|
bool(classic_hash and len(classic_hash) >= 8),
|
|
282
321
|
self._classicConnHash8 is not None,
|
|
283
322
|
auth_prefix,
|
|
284
323
|
ca51_prefix,
|
|
285
324
|
ca51_err,
|
|
286
325
|
auth_err,
|
|
326
|
+
ca52_notify_err,
|
|
327
|
+
ca53_notify_err,
|
|
328
|
+
c0002_prefix,
|
|
329
|
+
c0002_err,
|
|
330
|
+
c0003_notify_err,
|
|
287
331
|
)
|
|
288
332
|
|
|
289
333
|
classic_hash: bytes | None = None
|
|
@@ -305,7 +349,9 @@ class CasambiClient:
|
|
|
305
349
|
if classic_hash and len(classic_hash) >= 8:
|
|
306
350
|
self._protocolMode = ProtocolMode.CLASSIC
|
|
307
351
|
self._dataCharUuid = CASA_CLASSIC_DATA_CHAR_UUID
|
|
352
|
+
self._classicTxCharUuid = CASA_CLASSIC_DATA_CHAR_UUID
|
|
308
353
|
self._classicHeaderMode = "legacy"
|
|
354
|
+
self._classicHashSource = "ca51"
|
|
309
355
|
|
|
310
356
|
# Read connection hash (first 8 bytes are used for CMAC signing).
|
|
311
357
|
raw_hash = classic_hash
|
|
@@ -330,6 +376,7 @@ class CasambiClient:
|
|
|
330
376
|
**notify_kwargs,
|
|
331
377
|
)
|
|
332
378
|
except Exception as e:
|
|
379
|
+
ca52_notify_err = type(e).__name__
|
|
333
380
|
# Some firmwares may expose Classic signing on the EVO UUID instead.
|
|
334
381
|
# Fall through to auth-char probing if CA52 isn't available.
|
|
335
382
|
if self._logger.isEnabledFor(logging.DEBUG):
|
|
@@ -341,8 +388,25 @@ class CasambiClient:
|
|
|
341
388
|
self._protocolMode = None
|
|
342
389
|
self._dataCharUuid = None
|
|
343
390
|
self._classicConnHash8 = None
|
|
391
|
+
self._classicTxCharUuid = None
|
|
392
|
+
self._classicNotifyCharUuids.clear()
|
|
393
|
+
self._classicHeaderMode = None
|
|
394
|
+
self._classicHashSource = None
|
|
344
395
|
# continue detection below
|
|
345
396
|
else:
|
|
397
|
+
self._classicNotifyCharUuids.add(CASA_CLASSIC_DATA_CHAR_UUID.lower())
|
|
398
|
+
# Some Classic firmwares also expose state/config notifications on CA53.
|
|
399
|
+
try:
|
|
400
|
+
await self._gattClient.start_notify(
|
|
401
|
+
CASA_CLASSIC_CA53_CHAR_UUID,
|
|
402
|
+
self._queueCallback,
|
|
403
|
+
**notify_kwargs,
|
|
404
|
+
)
|
|
405
|
+
except Exception as e:
|
|
406
|
+
ca53_notify_err = type(e).__name__
|
|
407
|
+
else:
|
|
408
|
+
self._classicNotifyCharUuids.add(CASA_CLASSIC_CA53_CHAR_UUID.lower())
|
|
409
|
+
|
|
346
410
|
# Classic has no EVO-style key exchange/auth; we can send immediately.
|
|
347
411
|
self._connectionState = ConnectionState.AUTHENTICATED
|
|
348
412
|
self._logger.info("Protocol mode selected: CLASSIC")
|
|
@@ -354,10 +418,11 @@ class CasambiClient:
|
|
|
354
418
|
b2a(self._classicConnHash8),
|
|
355
419
|
)
|
|
356
420
|
self._logger.warning(
|
|
357
|
-
"[CASAMBI_CLASSIC_SELECTED] address=%s variant=ca52_legacy data_uuid=%s
|
|
421
|
+
"[CASAMBI_CLASSIC_SELECTED] address=%s variant=ca52_legacy data_uuid=%s tx_uuid=%s notify_uuids=%s header_mode=%s conn_hash8_prefix=%s",
|
|
358
422
|
self.address,
|
|
359
423
|
self._dataCharUuid,
|
|
360
|
-
|
|
424
|
+
self._classicTxCharUuid,
|
|
425
|
+
sorted(self._classicNotifyCharUuids) if self._classicNotifyCharUuids else None,
|
|
361
426
|
self._classicHeaderMode,
|
|
362
427
|
b2a(self._classicConnHash8),
|
|
363
428
|
)
|
|
@@ -368,6 +433,8 @@ class CasambiClient:
|
|
|
368
433
|
getattr(self._network, "isManager", lambda: False)(),
|
|
369
434
|
)
|
|
370
435
|
_log_probe_summary("CLASSIC", classic_variant="ca52_legacy")
|
|
436
|
+
# Emit a warning if we never see Classic RX frames; this is a common failure mode.
|
|
437
|
+
self._classicNoRxTask = asyncio.create_task(self._classic_no_rx_watchdog(30.0))
|
|
371
438
|
return
|
|
372
439
|
|
|
373
440
|
# Conformant devices can expose the Classic signed channel on the EVO-style UUID too.
|
|
@@ -438,34 +505,79 @@ class CasambiClient:
|
|
|
438
505
|
|
|
439
506
|
self._protocolMode = ProtocolMode.CLASSIC
|
|
440
507
|
self._dataCharUuid = CASA_AUTH_CHAR_UUID
|
|
508
|
+
self._classicTxCharUuid = CASA_AUTH_CHAR_UUID
|
|
441
509
|
self._classicHeaderMode = "conformant"
|
|
510
|
+
self._classicHashSource = "ca52_0001"
|
|
442
511
|
self._classicConnHash8 = bytes(first[:8])
|
|
443
512
|
self._classicCmdDiv = int.from_bytes(os.urandom(1), "big") or 1
|
|
444
513
|
self._classicTxSeq = 0
|
|
445
514
|
|
|
515
|
+
# Probe mapped Classic CA51 (0002) for diagnostics; some firmwares use it for time/config.
|
|
516
|
+
try:
|
|
517
|
+
v = await self._gattClient.read_gatt_char(CASA_CLASSIC_CONFORMANT_CA51_CHAR_UUID)
|
|
518
|
+
c0002_prefix = b2a(v[:10]) if v else None
|
|
519
|
+
if self._logger.isEnabledFor(logging.DEBUG):
|
|
520
|
+
self._logger.debug(
|
|
521
|
+
"[CASAMBI_GATT_PROBE] read classic-0002 ok len=%d prefix=%s",
|
|
522
|
+
0 if v is None else len(v),
|
|
523
|
+
c0002_prefix,
|
|
524
|
+
)
|
|
525
|
+
except Exception as e:
|
|
526
|
+
c0002_err = type(e).__name__
|
|
527
|
+
if self._logger.isEnabledFor(logging.DEBUG):
|
|
528
|
+
self._logger.debug(
|
|
529
|
+
"[CASAMBI_GATT_PROBE] read classic-0002 fail err=%s",
|
|
530
|
+
c0002_err,
|
|
531
|
+
)
|
|
532
|
+
|
|
446
533
|
notify_kwargs: dict[str, Any] = {}
|
|
447
534
|
notify_params = inspect.signature(self._gattClient.start_notify).parameters
|
|
448
535
|
if "bluez" in notify_params:
|
|
449
536
|
notify_kwargs["bluez"] = {"use_start_notify": True}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
537
|
+
try:
|
|
538
|
+
await self._gattClient.start_notify(
|
|
539
|
+
CASA_AUTH_CHAR_UUID,
|
|
540
|
+
self._queueCallback,
|
|
541
|
+
**notify_kwargs,
|
|
542
|
+
)
|
|
543
|
+
except Exception as e:
|
|
544
|
+
ca52_notify_err = type(e).__name__
|
|
545
|
+
else:
|
|
546
|
+
self._classicNotifyCharUuids.add(CASA_AUTH_CHAR_UUID.lower())
|
|
547
|
+
|
|
548
|
+
# Probe mapped Classic CA53 (0003) notify: some firmwares may emit state/config here.
|
|
549
|
+
try:
|
|
550
|
+
await self._gattClient.start_notify(
|
|
551
|
+
CASA_CLASSIC_CONFORMANT_CA53_CHAR_UUID,
|
|
552
|
+
self._queueCallback,
|
|
553
|
+
**notify_kwargs,
|
|
554
|
+
)
|
|
555
|
+
except Exception as e:
|
|
556
|
+
c0003_notify_err = type(e).__name__
|
|
557
|
+
else:
|
|
558
|
+
self._classicNotifyCharUuids.add(CASA_CLASSIC_CONFORMANT_CA53_CHAR_UUID.lower())
|
|
559
|
+
|
|
455
560
|
self._connectionState = ConnectionState.AUTHENTICATED
|
|
456
561
|
self._logger.info("Protocol mode selected: CLASSIC")
|
|
457
562
|
if self._logger.isEnabledFor(logging.DEBUG):
|
|
458
|
-
|
|
563
|
+
if ca52_notify_err is None:
|
|
564
|
+
self._logger.debug("[CASAMBI_GATT_PROBE] start_notify auth ok (classic conformant)")
|
|
565
|
+
else:
|
|
566
|
+
self._logger.debug(
|
|
567
|
+
"[CASAMBI_GATT_PROBE] start_notify auth fail err=%s (classic conformant)",
|
|
568
|
+
ca52_notify_err,
|
|
569
|
+
)
|
|
459
570
|
self._logger.debug(
|
|
460
571
|
"[CASAMBI_CLASSIC_CONN_HASH] len=%d hash=%s",
|
|
461
572
|
len(self._classicConnHash8),
|
|
462
573
|
b2a(self._classicConnHash8),
|
|
463
574
|
)
|
|
464
575
|
self._logger.warning(
|
|
465
|
-
"[CASAMBI_CLASSIC_SELECTED] address=%s variant=auth_uuid_conformant data_uuid=%s
|
|
576
|
+
"[CASAMBI_CLASSIC_SELECTED] address=%s variant=auth_uuid_conformant data_uuid=%s tx_uuid=%s notify_uuids=%s header_mode=%s conn_hash8_prefix=%s",
|
|
466
577
|
self.address,
|
|
467
578
|
self._dataCharUuid,
|
|
468
|
-
|
|
579
|
+
self._classicTxCharUuid,
|
|
580
|
+
sorted(self._classicNotifyCharUuids) if self._classicNotifyCharUuids else None,
|
|
469
581
|
self._classicHeaderMode,
|
|
470
582
|
b2a(self._classicConnHash8),
|
|
471
583
|
)
|
|
@@ -476,6 +588,7 @@ class CasambiClient:
|
|
|
476
588
|
getattr(self._network, "isManager", lambda: False)(),
|
|
477
589
|
)
|
|
478
590
|
_log_probe_summary("CLASSIC", classic_variant="auth_uuid_conformant")
|
|
591
|
+
self._classicNoRxTask = asyncio.create_task(self._classic_no_rx_watchdog(30.0))
|
|
479
592
|
return
|
|
480
593
|
|
|
481
594
|
_log_probe_summary("UNKNOWN")
|
|
@@ -483,12 +596,45 @@ class CasambiClient:
|
|
|
483
596
|
"No supported Casambi characteristics found (Classic ca51/ca52 or EVO/Classic-conformant auth char)."
|
|
484
597
|
)
|
|
485
598
|
|
|
599
|
+
async def _classic_no_rx_watchdog(self, after_s: float) -> None:
|
|
600
|
+
"""Emit one high-signal log if Classic RX stays silent after connect.
|
|
601
|
+
|
|
602
|
+
This helps testers capture actionable logs when Classic control/updates don't work yet.
|
|
603
|
+
"""
|
|
604
|
+
try:
|
|
605
|
+
await asyncio.sleep(after_s)
|
|
606
|
+
if self._protocolMode != ProtocolMode.CLASSIC:
|
|
607
|
+
return
|
|
608
|
+
if self._classicFirstRxTs is not None:
|
|
609
|
+
return
|
|
610
|
+
|
|
611
|
+
self._logger.warning(
|
|
612
|
+
"[CASAMBI_CLASSIC_NO_RX] after_s=%s notify_uuids=%s tx_uuid=%s header_mode=%s "
|
|
613
|
+
"conn_hash8_prefix=%s visitor=%s manager=%s cloud_session_is_manager=%s",
|
|
614
|
+
after_s,
|
|
615
|
+
sorted(self._classicNotifyCharUuids) if self._classicNotifyCharUuids else None,
|
|
616
|
+
self._classicTxCharUuid,
|
|
617
|
+
self._classicHeaderMode,
|
|
618
|
+
None if self._classicConnHash8 is None else b2a(self._classicConnHash8),
|
|
619
|
+
self._network.classicVisitorKey() is not None,
|
|
620
|
+
self._network.classicManagerKey() is not None,
|
|
621
|
+
getattr(self._network, "isManager", lambda: False)(),
|
|
622
|
+
)
|
|
623
|
+
except asyncio.CancelledError:
|
|
624
|
+
return
|
|
625
|
+
except Exception:
|
|
626
|
+
# Never fail the connection because of diagnostics.
|
|
627
|
+
self._logger.debug("Classic no-RX watchdog failed.", exc_info=True)
|
|
628
|
+
|
|
486
629
|
def _on_disconnect(self, client: BleakClient) -> None:
|
|
487
630
|
if self._connectionState != ConnectionState.NONE:
|
|
488
631
|
self._logger.info(f"Received disconnect callback from {self.address}")
|
|
489
632
|
if self._connectionState == ConnectionState.AUTHENTICATED:
|
|
490
633
|
self._logger.debug("Executing disconnect callback.")
|
|
491
634
|
self._disconnectedCallback()
|
|
635
|
+
if self._classicNoRxTask is not None:
|
|
636
|
+
self._classicNoRxTask.cancel()
|
|
637
|
+
self._classicNoRxTask = None
|
|
492
638
|
self._connectionState = ConnectionState.NONE
|
|
493
639
|
|
|
494
640
|
async def exchangeKey(self) -> None:
|
|
@@ -862,8 +1008,9 @@ class CasambiClient:
|
|
|
862
1008
|
self._checkState(ConnectionState.AUTHENTICATED)
|
|
863
1009
|
if self._protocolMode != ProtocolMode.CLASSIC:
|
|
864
1010
|
raise ProtocolError("Classic send called while not in Classic protocol mode.")
|
|
865
|
-
|
|
866
|
-
|
|
1011
|
+
tx_uuid = self._classicTxCharUuid or self._dataCharUuid
|
|
1012
|
+
if not tx_uuid:
|
|
1013
|
+
raise ProtocolError("Classic TX characteristic UUID not set.")
|
|
867
1014
|
if self._classicConnHash8 is None:
|
|
868
1015
|
raise ClassicHandshakeError("Classic connection hash not available.")
|
|
869
1016
|
|
|
@@ -974,12 +1121,13 @@ class CasambiClient:
|
|
|
974
1121
|
if self._logLimiter.allow("classic_tx", burst=50, window_s=60.0):
|
|
975
1122
|
auth_str = f"0x{auth_level:02x}" if header_mode == "conformant" else None
|
|
976
1123
|
self._logger.warning(
|
|
977
|
-
"[CASAMBI_CLASSIC_TX] header=%s key=%s signed=%s auth=%s sig_len=%d seq=%s "
|
|
1124
|
+
"[CASAMBI_CLASSIC_TX] header=%s key=%s signed=%s tx_uuid=%s auth=%s sig_len=%d seq=%s "
|
|
978
1125
|
"cmd_len=%d cmd_ord=%s target=%s div=%s lifetime=%s payload_len=%s "
|
|
979
1126
|
"total_len=%d prefix=%s",
|
|
980
1127
|
header_mode,
|
|
981
1128
|
key_name,
|
|
982
1129
|
signed,
|
|
1130
|
+
tx_uuid,
|
|
983
1131
|
auth_str,
|
|
984
1132
|
sig_len,
|
|
985
1133
|
None if seq is None else f"0x{seq:04x}",
|
|
@@ -995,11 +1143,20 @@ class CasambiClient:
|
|
|
995
1143
|
|
|
996
1144
|
# Classic packets can exceed 20 bytes when using a 16-byte manager signature.
|
|
997
1145
|
# Bleak needs a write-with-response for long writes on most backends.
|
|
998
|
-
await self._gattClient.write_gatt_char(
|
|
1146
|
+
await self._gattClient.write_gatt_char(tx_uuid, bytes(pkt), response=True)
|
|
999
1147
|
|
|
1000
1148
|
def _establishedNofityCallback(
|
|
1001
1149
|
self, handle: BleakGATTCharacteristic, data: bytes
|
|
1002
1150
|
) -> None:
|
|
1151
|
+
# Route notifications based on characteristic UUID when available.
|
|
1152
|
+
# This helps with mixed/legacy setups where multiple Classic channels might be active.
|
|
1153
|
+
try:
|
|
1154
|
+
handle_uuid = str(getattr(handle, "uuid", "")).lower()
|
|
1155
|
+
except Exception:
|
|
1156
|
+
handle_uuid = ""
|
|
1157
|
+
if handle_uuid and handle_uuid in self._classicNotifyCharUuids:
|
|
1158
|
+
self._classicEstablishedNotifyCallback(handle, data)
|
|
1159
|
+
return
|
|
1003
1160
|
if self._protocolMode == ProtocolMode.CLASSIC:
|
|
1004
1161
|
self._classicEstablishedNotifyCallback(handle, data)
|
|
1005
1162
|
return
|
|
@@ -1074,6 +1231,8 @@ class CasambiClient:
|
|
|
1074
1231
|
"""
|
|
1075
1232
|
self._inPacketCount += 1
|
|
1076
1233
|
self._classicRxFrames += 1
|
|
1234
|
+
if self._classicFirstRxTs is None:
|
|
1235
|
+
self._classicFirstRxTs = time.monotonic()
|
|
1077
1236
|
|
|
1078
1237
|
raw = bytes(data)
|
|
1079
1238
|
if self._logger.isEnabledFor(logging.DEBUG):
|
|
@@ -1681,6 +1840,10 @@ class CasambiClient:
|
|
|
1681
1840
|
async def disconnect(self) -> None:
|
|
1682
1841
|
self._logger.info("Disconnecting...")
|
|
1683
1842
|
|
|
1843
|
+
if self._classicNoRxTask is not None:
|
|
1844
|
+
self._classicNoRxTask.cancel()
|
|
1845
|
+
self._classicNoRxTask = None
|
|
1846
|
+
|
|
1684
1847
|
if self._callbackTask is not None:
|
|
1685
1848
|
# Cancel and await the background callback task to avoid
|
|
1686
1849
|
# 'Task was destroyed but it is pending' warnings.
|
CasambiBt/_constants.py
CHANGED
|
@@ -12,6 +12,15 @@ CASA_AUTH_CHAR_UUID: Final = "c9ffde48-ca5a-0001-ab83-8f519b482f77"
|
|
|
12
12
|
CASA_UUID_CLASSIC: Final = "0000ca5a-0000-1000-8000-00805f9b34fb"
|
|
13
13
|
CASA_CLASSIC_HASH_CHAR_UUID: Final = "0000ca51-0000-1000-8000-00805f9b34fb"
|
|
14
14
|
CASA_CLASSIC_DATA_CHAR_UUID: Final = "0000ca52-0000-1000-8000-00805f9b34fb"
|
|
15
|
+
CASA_CLASSIC_CA53_CHAR_UUID: Final = "0000ca53-0000-1000-8000-00805f9b34fb"
|
|
16
|
+
|
|
17
|
+
# Classic "conformant" firmware maps the legacy CA5A/CA5x UUIDs onto the FE4D service.
|
|
18
|
+
# Ground truth: casambi-android `t1.C1713d.e(UUID)` mapping:
|
|
19
|
+
# - CA52 -> 0001 (same as CASA_AUTH_CHAR_UUID)
|
|
20
|
+
# - CA51 -> 0002
|
|
21
|
+
# - CA53 -> 0003
|
|
22
|
+
CASA_CLASSIC_CONFORMANT_CA51_CHAR_UUID: Final = "c9ffde48-ca5a-0002-ab83-8f519b482f77"
|
|
23
|
+
CASA_CLASSIC_CONFORMANT_CA53_CHAR_UUID: Final = "c9ffde48-ca5a-0003-ab83-8f519b482f77"
|
|
15
24
|
|
|
16
25
|
|
|
17
26
|
@unique
|
CasambiBt/_network.py
CHANGED
|
@@ -291,7 +291,7 @@ class Network:
|
|
|
291
291
|
"[CASAMBI_CLOUD_UPDATE_RETRY] status=400 retry_with_token_clientInfo=true body_prefix=%r",
|
|
292
292
|
(res.text or "")[:200],
|
|
293
293
|
)
|
|
294
|
-
payload2 = dict(payload)
|
|
294
|
+
payload2: dict[str, Any] = dict(payload)
|
|
295
295
|
payload2["token"] = self._token
|
|
296
296
|
payload2["clientInfo"] = self._clientInfo
|
|
297
297
|
res = await self._httpClient.put(
|
|
@@ -301,26 +301,33 @@ class Network:
|
|
|
301
301
|
)
|
|
302
302
|
|
|
303
303
|
if res.status_code != httpx.codes.OK:
|
|
304
|
-
|
|
305
|
-
|
|
304
|
+
body_prefix = (res.text or "")[:500]
|
|
305
|
+
# If we have cached network data, do not fail setup; continue offline.
|
|
306
|
+
# This is important for HA stability and for "cloud down / API changed" scenarios.
|
|
307
|
+
have_cache = bool(self._networkRevision and self._networkRevision > 0 and self._rawNetworkData)
|
|
308
|
+
self._logger.warning(
|
|
309
|
+
"[CASAMBI_CLOUD_UPDATE_FAILED] status=%s cached_revision=%s continuing_offline=%s body_prefix=%r",
|
|
306
310
|
res.status_code,
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
self._logger.debug(f"Network: {res.text}")
|
|
312
|
-
|
|
313
|
-
updateResult = res.json()
|
|
314
|
-
if updateResult["status"] != "UPTODATE":
|
|
315
|
-
self._networkRevision = updateResult["network"]["revision"]
|
|
316
|
-
self._rawNetworkData = updateResult
|
|
317
|
-
async with self._cache as cachePath:
|
|
318
|
-
cachedNetworkPah = cachePath / f"{self._id}.json"
|
|
319
|
-
await cachedNetworkPah.write_bytes(res.content)
|
|
320
|
-
network = updateResult
|
|
321
|
-
self._logger.info(
|
|
322
|
-
f"Fetched updated network with revision {self._networkRevision}"
|
|
311
|
+
self._networkRevision,
|
|
312
|
+
have_cache,
|
|
313
|
+
body_prefix,
|
|
323
314
|
)
|
|
315
|
+
if not have_cache:
|
|
316
|
+
raise NetworkUpdateError("Could not update network!")
|
|
317
|
+
else:
|
|
318
|
+
self._logger.debug(f"Network: {res.text}")
|
|
319
|
+
|
|
320
|
+
updateResult = res.json()
|
|
321
|
+
if updateResult["status"] != "UPTODATE":
|
|
322
|
+
self._networkRevision = updateResult["network"]["revision"]
|
|
323
|
+
self._rawNetworkData = updateResult
|
|
324
|
+
async with self._cache as cachePath:
|
|
325
|
+
cachedNetworkPah = cachePath / f"{self._id}.json"
|
|
326
|
+
await cachedNetworkPah.write_bytes(res.content)
|
|
327
|
+
network = updateResult
|
|
328
|
+
self._logger.info(
|
|
329
|
+
f"Fetched updated network with revision {self._networkRevision}"
|
|
330
|
+
)
|
|
324
331
|
except RequestError as err:
|
|
325
332
|
if self._networkRevision == 0:
|
|
326
333
|
raise NetworkUpdateError from err
|
CasambiBt/_version.py
CHANGED
{casambi_bt_revamped-0.3.12.dev8.dist-info → casambi_bt_revamped-0.3.12.dev9.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: casambi-bt-revamped
|
|
3
|
-
Version: 0.3.12.
|
|
3
|
+
Version: 0.3.12.dev9
|
|
4
4
|
Summary: Forked Casambi Bluetooth client library with switch event support, use original if no special need. https://github.com/lkempf/casambi-bt
|
|
5
5
|
Home-page: https://github.com/rankjie/casambi-bt
|
|
6
6
|
Author: rankjie
|
{casambi_bt_revamped-0.3.12.dev8.dist-info → casambi_bt_revamped-0.3.12.dev9.dist-info}/RECORD
RENAMED
|
@@ -2,21 +2,21 @@ CasambiBt/__init__.py,sha256=iJdTF4oeXfj5d5gfGxQkacqUjtnQo0IW-zFPJvFjWWk,336
|
|
|
2
2
|
CasambiBt/_cache.py,sha256=3bQil8vhSy4f4sf9JusMfEdQC7d3cJuva9qHhyKro-0,3808
|
|
3
3
|
CasambiBt/_casambi.py,sha256=TN4ecgjm95nSJ4h9TsKayNn577Y82fdsGK4IGUZF23Q,40666
|
|
4
4
|
CasambiBt/_classic_crypto.py,sha256=6DcCOdjLQo7k2cOOutNdUKupykOG_E2TDDwg6fH-ODM,998
|
|
5
|
-
CasambiBt/_client.py,sha256=
|
|
6
|
-
CasambiBt/_constants.py,sha256=
|
|
5
|
+
CasambiBt/_client.py,sha256=yn6DckDCKap1YJ0xyDhk5wlvE6wvPIUGPe9M1veRDVs,77317
|
|
6
|
+
CasambiBt/_constants.py,sha256=86heoDdb5iPaRrPmK2DIIl-4uSxbFFcnCo9zlCvTLww,1290
|
|
7
7
|
CasambiBt/_discover.py,sha256=jLc6H69JddrCURgtANZEjws6_UbSzXJtvJkbKTaIUHY,1849
|
|
8
8
|
CasambiBt/_encryption.py,sha256=CLcoOOrggQqhJbnr_emBnEnkizpWDvb_0yFnitq4_FM,3831
|
|
9
9
|
CasambiBt/_invocation.py,sha256=fkG4R0Gv5_amFfD_P6DKuIEe3oKWZW0v8RSU8zDjPdI,2985
|
|
10
10
|
CasambiBt/_keystore.py,sha256=Jdiq0zMPDmhfpheSojKY6sTUpmVrvX_qOyO7yCYd3kw,2788
|
|
11
|
-
CasambiBt/_network.py,sha256=
|
|
11
|
+
CasambiBt/_network.py,sha256=ai1o3EybsAhjyPohSOxeE0cWoFvEqdcc3PE3uFDaTfE,21346
|
|
12
12
|
CasambiBt/_operation.py,sha256=Q5UccsrtNp_B_wWqwH_3eLFW_yF6A55FMmfUKDk2WrI,1059
|
|
13
13
|
CasambiBt/_switch_events.py,sha256=S8OD0dBcw5T4J2C7qfmOQMnTJ7omIXRUYv4PqDOB87E,13137
|
|
14
14
|
CasambiBt/_unit.py,sha256=nxbg_8UCCVB9WI8dUS21g2JrGyPKcefqKMSusMOhLOo,18721
|
|
15
|
-
CasambiBt/_version.py,sha256=
|
|
15
|
+
CasambiBt/_version.py,sha256=VtWwYBijhWhHtBHLmr-n1eqgiJjNkNATaUweLPXTAo4,337
|
|
16
16
|
CasambiBt/errors.py,sha256=1L_Q8og_N_BRYEKizghAQXr6tihlHykFgtcCHUDcBas,1961
|
|
17
17
|
CasambiBt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
-
casambi_bt_revamped-0.3.12.
|
|
19
|
-
casambi_bt_revamped-0.3.12.
|
|
20
|
-
casambi_bt_revamped-0.3.12.
|
|
21
|
-
casambi_bt_revamped-0.3.12.
|
|
22
|
-
casambi_bt_revamped-0.3.12.
|
|
18
|
+
casambi_bt_revamped-0.3.12.dev9.dist-info/licenses/LICENSE,sha256=TAIIitFxpxEDi6Iju7foW4TDQmWvC-IhLVLhl67jKmQ,11341
|
|
19
|
+
casambi_bt_revamped-0.3.12.dev9.dist-info/METADATA,sha256=R31XEvi4eCtF_CYH_DfCnN9lvMmhDS8zCYJlqFAuu2U,5877
|
|
20
|
+
casambi_bt_revamped-0.3.12.dev9.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
21
|
+
casambi_bt_revamped-0.3.12.dev9.dist-info/top_level.txt,sha256=uNbqLjtecFosoFzpGAC89-5icikWODKI8rOjbi8v_sA,10
|
|
22
|
+
casambi_bt_revamped-0.3.12.dev9.dist-info/RECORD,,
|
{casambi_bt_revamped-0.3.12.dev8.dist-info → casambi_bt_revamped-0.3.12.dev9.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|