casambi-bt-revamped 0.3.12.dev4__py3-none-any.whl → 0.3.12.dev5__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 +163 -14
- {casambi_bt_revamped-0.3.12.dev4.dist-info → casambi_bt_revamped-0.3.12.dev5.dist-info}/METADATA +1 -1
- {casambi_bt_revamped-0.3.12.dev4.dist-info → casambi_bt_revamped-0.3.12.dev5.dist-info}/RECORD +6 -6
- {casambi_bt_revamped-0.3.12.dev4.dist-info → casambi_bt_revamped-0.3.12.dev5.dist-info}/WHEEL +0 -0
- {casambi_bt_revamped-0.3.12.dev4.dist-info → casambi_bt_revamped-0.3.12.dev5.dist-info}/licenses/LICENSE +0 -0
- {casambi_bt_revamped-0.3.12.dev4.dist-info → casambi_bt_revamped-0.3.12.dev5.dist-info}/top_level.txt +0 -0
CasambiBt/_client.py
CHANGED
|
@@ -100,6 +100,8 @@ class CasambiClient:
|
|
|
100
100
|
# Determined at runtime by inspecting GATT services/characteristics.
|
|
101
101
|
self._protocolMode: ProtocolMode | None = None
|
|
102
102
|
self._dataCharUuid: str | None = None
|
|
103
|
+
# EVO only: protocolVersion from the device-provided NodeInfo (byte1).
|
|
104
|
+
self._deviceProtocolVersion: int | None = None
|
|
103
105
|
|
|
104
106
|
# Classic protocol state
|
|
105
107
|
self._classicConnHash8: bytes | None = None
|
|
@@ -119,14 +121,29 @@ class CasambiClient:
|
|
|
119
121
|
def protocolMode(self) -> ProtocolMode | None:
|
|
120
122
|
return self._protocolMode
|
|
121
123
|
|
|
122
|
-
def _checkProtocolVersion(self, version: int) -> None:
|
|
124
|
+
def _checkProtocolVersion(self, version: int, *, source: str = "unknown") -> None:
|
|
125
|
+
strict = os.getenv("CASAMBI_BT_STRICT_PROTOCOL_VERSION", "").strip() in {
|
|
126
|
+
"1",
|
|
127
|
+
"true",
|
|
128
|
+
"TRUE",
|
|
129
|
+
"yes",
|
|
130
|
+
"YES",
|
|
131
|
+
}
|
|
123
132
|
if version < MIN_VERSION:
|
|
124
|
-
|
|
125
|
-
|
|
133
|
+
# Legacy protocol versions are intentionally allowed. We keep this check as a warning
|
|
134
|
+
# because packet layouts/handshakes may differ and we want actionable tester logs.
|
|
135
|
+
msg = (
|
|
136
|
+
f"Legacy protocol version detected ({source}={version}). "
|
|
137
|
+
f"Versions < {MIN_VERSION} are not fully verified; attempting to continue."
|
|
126
138
|
)
|
|
139
|
+
if strict:
|
|
140
|
+
raise UnsupportedProtocolVersion(msg)
|
|
141
|
+
self._logger.warning(msg)
|
|
142
|
+
return
|
|
127
143
|
if version > MAX_VERSION:
|
|
128
144
|
self._logger.warning(
|
|
129
|
-
"Version too new
|
|
145
|
+
"Version too new (%s=%i). Highest supported version is %i. Continue at your own risk.",
|
|
146
|
+
source,
|
|
130
147
|
version,
|
|
131
148
|
MAX_VERSION,
|
|
132
149
|
)
|
|
@@ -217,11 +234,45 @@ class CasambiClient:
|
|
|
217
234
|
# 2) EVO: auth char read starts with 0x01 (NodeInfo)
|
|
218
235
|
# 3) Classic "conformant": auth char read returns connection hash (first 8 bytes used)
|
|
219
236
|
|
|
237
|
+
cloud_protocol = getattr(self._network, "protocolVersion", None)
|
|
238
|
+
ca51_prefix: bytes | None = None
|
|
239
|
+
ca51_err: str | None = None
|
|
240
|
+
auth_prefix: bytes | None = None
|
|
241
|
+
auth_err: str | None = None
|
|
242
|
+
device_nodeinfo_protocol: int | None = None
|
|
243
|
+
|
|
244
|
+
def _log_probe_summary(mode: str) -> None:
|
|
245
|
+
# One stable, high-signal line for testers.
|
|
246
|
+
self._logger.info(
|
|
247
|
+
"[CASAMBI_PROTOCOL_PROBE] address=%s mode=%s cloud_protocol=%s device_nodeinfo_protocol=%s "
|
|
248
|
+
"data_uuid=%s classic_hash8_present=%s auth_read_prefix=%s ca51_read_prefix=%s ca51_read_error=%s auth_read_error=%s",
|
|
249
|
+
self.address,
|
|
250
|
+
mode,
|
|
251
|
+
cloud_protocol,
|
|
252
|
+
device_nodeinfo_protocol,
|
|
253
|
+
self._dataCharUuid,
|
|
254
|
+
bool(classic_hash and len(classic_hash) >= 8),
|
|
255
|
+
auth_prefix,
|
|
256
|
+
ca51_prefix,
|
|
257
|
+
ca51_err,
|
|
258
|
+
auth_err,
|
|
259
|
+
)
|
|
260
|
+
|
|
220
261
|
classic_hash: bytes | None = None
|
|
221
262
|
try:
|
|
222
263
|
classic_hash = await self._gattClient.read_gatt_char(CASA_CLASSIC_HASH_CHAR_UUID)
|
|
223
|
-
|
|
264
|
+
ca51_prefix = b2a(classic_hash[:10]) if classic_hash else None
|
|
265
|
+
if self._logger.isEnabledFor(logging.DEBUG):
|
|
266
|
+
self._logger.debug(
|
|
267
|
+
"[CASAMBI_GATT_PROBE] read ca51 ok len=%d prefix=%s",
|
|
268
|
+
0 if classic_hash is None else len(classic_hash),
|
|
269
|
+
ca51_prefix,
|
|
270
|
+
)
|
|
271
|
+
except Exception as e:
|
|
224
272
|
classic_hash = None
|
|
273
|
+
ca51_err = type(e).__name__
|
|
274
|
+
if self._logger.isEnabledFor(logging.DEBUG):
|
|
275
|
+
self._logger.debug("[CASAMBI_GATT_PROBE] read ca51 fail err=%s", ca51_err)
|
|
225
276
|
|
|
226
277
|
if classic_hash and len(classic_hash) >= 8:
|
|
227
278
|
if os.getenv("CASAMBI_BT_DISABLE_CLASSIC", "").strip() in {"1", "true", "TRUE", "yes", "YES"}:
|
|
@@ -260,7 +311,12 @@ class CasambiClient:
|
|
|
260
311
|
except Exception as e:
|
|
261
312
|
# Some firmwares may expose Classic signing on the EVO UUID instead.
|
|
262
313
|
# Fall through to auth-char probing if CA52 isn't available.
|
|
263
|
-
self._logger.
|
|
314
|
+
if self._logger.isEnabledFor(logging.DEBUG):
|
|
315
|
+
self._logger.debug(
|
|
316
|
+
"[CASAMBI_GATT_PROBE] start_notify ca52 fail err=%s; trying auth UUID probing.",
|
|
317
|
+
type(e).__name__,
|
|
318
|
+
exc_info=True,
|
|
319
|
+
)
|
|
264
320
|
self._protocolMode = None
|
|
265
321
|
self._dataCharUuid = None
|
|
266
322
|
self._classicConnHash8 = None
|
|
@@ -270,26 +326,78 @@ class CasambiClient:
|
|
|
270
326
|
self._connectionState = ConnectionState.AUTHENTICATED
|
|
271
327
|
self._logger.info("Protocol mode selected: CLASSIC")
|
|
272
328
|
if self._logger.isEnabledFor(logging.DEBUG):
|
|
329
|
+
self._logger.debug("[CASAMBI_GATT_PROBE] start_notify ca52 ok")
|
|
273
330
|
self._logger.debug(
|
|
274
331
|
"[CASAMBI_CLASSIC_CONN_HASH] len=%d hash=%s",
|
|
275
332
|
len(self._classicConnHash8),
|
|
276
333
|
b2a(self._classicConnHash8),
|
|
277
334
|
)
|
|
335
|
+
_log_probe_summary("CLASSIC")
|
|
278
336
|
return
|
|
279
337
|
|
|
280
338
|
# Conformant devices can expose the Classic signed channel on the EVO-style UUID too.
|
|
281
339
|
first: bytes | None = None
|
|
282
340
|
try:
|
|
283
341
|
first = await self._gattClient.read_gatt_char(CASA_AUTH_CHAR_UUID)
|
|
284
|
-
|
|
342
|
+
auth_prefix = b2a(first[:10]) if first else None
|
|
343
|
+
if self._logger.isEnabledFor(logging.DEBUG):
|
|
344
|
+
self._logger.debug(
|
|
345
|
+
"[CASAMBI_GATT_PROBE] read auth ok len=%d first_byte=%s prefix=%s",
|
|
346
|
+
0 if first is None else len(first),
|
|
347
|
+
None if not first else f"0x{first[0]:02x}",
|
|
348
|
+
auth_prefix,
|
|
349
|
+
)
|
|
350
|
+
except Exception as e:
|
|
285
351
|
first = None
|
|
352
|
+
auth_err = type(e).__name__
|
|
353
|
+
if self._logger.isEnabledFor(logging.DEBUG):
|
|
354
|
+
self._logger.debug("[CASAMBI_GATT_PROBE] read auth fail err=%s", auth_err)
|
|
286
355
|
|
|
287
356
|
if first and len(first) >= 2 and first[0] == 0x01:
|
|
288
357
|
# EVO NodeInfo packet starts with 0x01.
|
|
358
|
+
device_nodeinfo_protocol = first[1]
|
|
359
|
+
self._deviceProtocolVersion = device_nodeinfo_protocol
|
|
360
|
+
mtu = unit = flags = None
|
|
361
|
+
nonce_prefix = None
|
|
362
|
+
if len(first) >= 23:
|
|
363
|
+
try:
|
|
364
|
+
mtu, unit, flags, nonce = struct.unpack_from(">BHH16s", first, 2)
|
|
365
|
+
nonce_prefix = b2a(nonce[:8])
|
|
366
|
+
except Exception:
|
|
367
|
+
if self._logger.isEnabledFor(logging.DEBUG):
|
|
368
|
+
self._logger.debug("Failed to parse NodeInfo fields for logging.", exc_info=True)
|
|
369
|
+
|
|
370
|
+
self._logger.info(
|
|
371
|
+
"[CASAMBI_EVO_NODEINFO] cloud_protocol=%s device_protocol=%s mtu=%s unit=%s flags=%s nonce_prefix=%s len=%d prefix=%s",
|
|
372
|
+
cloud_protocol,
|
|
373
|
+
device_nodeinfo_protocol,
|
|
374
|
+
mtu,
|
|
375
|
+
unit,
|
|
376
|
+
None if flags is None else f"0x{flags:04x}",
|
|
377
|
+
nonce_prefix,
|
|
378
|
+
len(first),
|
|
379
|
+
b2a(first[: min(len(first), 32)]),
|
|
380
|
+
)
|
|
381
|
+
if cloud_protocol is not None and device_nodeinfo_protocol != cloud_protocol:
|
|
382
|
+
self._logger.warning(
|
|
383
|
+
"[CASAMBI_EVO_NODEINFO_MISMATCH] cloud_protocol=%s device_protocol=%s",
|
|
384
|
+
cloud_protocol,
|
|
385
|
+
device_nodeinfo_protocol,
|
|
386
|
+
)
|
|
387
|
+
if len(first) < 23:
|
|
388
|
+
self._logger.warning(
|
|
389
|
+
"[CASAMBI_EVO_NODEINFO_SHORT] len=%d cloud_protocol=%s device_protocol=%s prefix=%s",
|
|
390
|
+
len(first),
|
|
391
|
+
cloud_protocol,
|
|
392
|
+
device_nodeinfo_protocol,
|
|
393
|
+
b2a(first[: min(len(first), 32)]),
|
|
394
|
+
)
|
|
395
|
+
|
|
289
396
|
self._protocolMode = ProtocolMode.EVO
|
|
290
397
|
self._dataCharUuid = CASA_AUTH_CHAR_UUID
|
|
291
|
-
self._checkProtocolVersion(
|
|
398
|
+
self._checkProtocolVersion(device_nodeinfo_protocol, source="device_nodeinfo")
|
|
292
399
|
self._logger.info("Protocol mode selected: EVO")
|
|
400
|
+
_log_probe_summary("EVO")
|
|
293
401
|
return
|
|
294
402
|
|
|
295
403
|
if first is not None:
|
|
@@ -323,11 +431,13 @@ class CasambiClient:
|
|
|
323
431
|
self._connectionState = ConnectionState.AUTHENTICATED
|
|
324
432
|
self._logger.info("Protocol mode selected: CLASSIC")
|
|
325
433
|
if self._logger.isEnabledFor(logging.DEBUG):
|
|
434
|
+
self._logger.debug("[CASAMBI_GATT_PROBE] start_notify auth ok (classic conformant)")
|
|
326
435
|
self._logger.debug(
|
|
327
436
|
"[CASAMBI_CLASSIC_CONN_HASH] len=%d hash=%s",
|
|
328
437
|
len(self._classicConnHash8),
|
|
329
438
|
b2a(self._classicConnHash8),
|
|
330
439
|
)
|
|
440
|
+
_log_probe_summary("CLASSIC")
|
|
331
441
|
return
|
|
332
442
|
|
|
333
443
|
raise ProtocolError(
|
|
@@ -351,15 +461,54 @@ class CasambiClient:
|
|
|
351
461
|
try:
|
|
352
462
|
# Initiate communication with device
|
|
353
463
|
firstResp = await self._gattClient.read_gatt_char(CASA_AUTH_CHAR_UUID)
|
|
354
|
-
self._logger.
|
|
464
|
+
if self._logger.isEnabledFor(logging.DEBUG):
|
|
465
|
+
self._logger.debug(
|
|
466
|
+
"[CASAMBI_EVO_NODEINFO_RAW] len=%d prefix=%s",
|
|
467
|
+
len(firstResp),
|
|
468
|
+
b2a(firstResp[: min(len(firstResp), 32)]),
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
cloud_protocol = getattr(self._network, "protocolVersion", None)
|
|
472
|
+
expected_protocol = self._deviceProtocolVersion or cloud_protocol
|
|
473
|
+
|
|
474
|
+
# EVO key exchange expects the NodeInfo packet (0x01 ...).
|
|
475
|
+
if len(firstResp) < 2 or firstResp[0] != 0x01:
|
|
476
|
+
self._logger.error(
|
|
477
|
+
"[CASAMBI_EVO_NODEINFO_UNEXPECTED] expected_prefix=01 len=%d prefix=%s",
|
|
478
|
+
len(firstResp),
|
|
479
|
+
b2a(firstResp[: min(len(firstResp), 32)]),
|
|
480
|
+
)
|
|
481
|
+
raise ProtocolError("Unexpected NodeInfo response while starting key exchange.")
|
|
482
|
+
|
|
483
|
+
device_protocol = firstResp[1]
|
|
484
|
+
self._deviceProtocolVersion = device_protocol
|
|
485
|
+
self._checkProtocolVersion(device_protocol, source="device_nodeinfo")
|
|
486
|
+
|
|
487
|
+
if expected_protocol is not None and device_protocol != expected_protocol:
|
|
488
|
+
self._logger.warning(
|
|
489
|
+
"[CASAMBI_EVO_NODEINFO_MISMATCH] expected_protocol=%s cloud_protocol=%s device_protocol=%s",
|
|
490
|
+
expected_protocol,
|
|
491
|
+
cloud_protocol,
|
|
492
|
+
device_protocol,
|
|
493
|
+
)
|
|
494
|
+
elif cloud_protocol is not None and device_protocol != cloud_protocol:
|
|
495
|
+
# Keep this separate to catch cloud/device mismatches even if we didn't have an expected protocol set.
|
|
496
|
+
self._logger.warning(
|
|
497
|
+
"[CASAMBI_EVO_NODEINFO_MISMATCH] expected_protocol=%s cloud_protocol=%s device_protocol=%s",
|
|
498
|
+
expected_protocol,
|
|
499
|
+
cloud_protocol,
|
|
500
|
+
device_protocol,
|
|
501
|
+
)
|
|
355
502
|
|
|
356
|
-
|
|
357
|
-
if not (
|
|
358
|
-
firstResp[0] == 0x1 and firstResp[1] == self._network.protocolVersion
|
|
359
|
-
):
|
|
503
|
+
if len(firstResp) < 23:
|
|
360
504
|
self._logger.error(
|
|
361
|
-
"
|
|
505
|
+
"[CASAMBI_EVO_NODEINFO_SHORT] len=%d cloud_protocol=%s device_protocol=%s prefix=%s",
|
|
506
|
+
len(firstResp),
|
|
507
|
+
cloud_protocol,
|
|
508
|
+
device_protocol,
|
|
509
|
+
b2a(firstResp[: min(len(firstResp), 32)]),
|
|
362
510
|
)
|
|
511
|
+
raise ProtocolError("NodeInfo response too short while starting key exchange.")
|
|
363
512
|
|
|
364
513
|
# Parse device info
|
|
365
514
|
self._mtu, self._unit, self._flags, self._nonce = struct.unpack_from(
|
{casambi_bt_revamped-0.3.12.dev4.dist-info → casambi_bt_revamped-0.3.12.dev5.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.dev5
|
|
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.dev4.dist-info → casambi_bt_revamped-0.3.12.dev5.dist-info}/RECORD
RENAMED
|
@@ -2,7 +2,7 @@ CasambiBt/__init__.py,sha256=TW445xSu5PV3TyMjJfwaA1JoWvQQ8LXhZgGdDTfWf3s,302
|
|
|
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=
|
|
5
|
+
CasambiBt/_client.py,sha256=AASUN9OvmTIg9IeYMEvLI8kBEYbV9FapIuyDXGZMpME,57883
|
|
6
6
|
CasambiBt/_constants.py,sha256=sbElg5W8eeQvvL1rHn_E0jhP1wOrrabc7dFLLnlDMsU,810
|
|
7
7
|
CasambiBt/_discover.py,sha256=jLc6H69JddrCURgtANZEjws6_UbSzXJtvJkbKTaIUHY,1849
|
|
8
8
|
CasambiBt/_encryption.py,sha256=CLcoOOrggQqhJbnr_emBnEnkizpWDvb_0yFnitq4_FM,3831
|
|
@@ -14,8 +14,8 @@ CasambiBt/_switch_events.py,sha256=S8OD0dBcw5T4J2C7qfmOQMnTJ7omIXRUYv4PqDOB87E,1
|
|
|
14
14
|
CasambiBt/_unit.py,sha256=KIpvUT_Wm-O2Lmb1JVnNO625-j5j7GqufmZzfTR-jW0,18587
|
|
15
15
|
CasambiBt/errors.py,sha256=1L_Q8og_N_BRYEKizghAQXr6tihlHykFgtcCHUDcBas,1961
|
|
16
16
|
CasambiBt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
-
casambi_bt_revamped-0.3.12.
|
|
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.
|
|
17
|
+
casambi_bt_revamped-0.3.12.dev5.dist-info/licenses/LICENSE,sha256=TAIIitFxpxEDi6Iju7foW4TDQmWvC-IhLVLhl67jKmQ,11341
|
|
18
|
+
casambi_bt_revamped-0.3.12.dev5.dist-info/METADATA,sha256=mNRrJjPdZBbSvEJp9RBAYkv7wU0-znKwBhpR5XEXtLo,5877
|
|
19
|
+
casambi_bt_revamped-0.3.12.dev5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
20
|
+
casambi_bt_revamped-0.3.12.dev5.dist-info/top_level.txt,sha256=uNbqLjtecFosoFzpGAC89-5icikWODKI8rOjbi8v_sA,10
|
|
21
|
+
casambi_bt_revamped-0.3.12.dev5.dist-info/RECORD,,
|
{casambi_bt_revamped-0.3.12.dev4.dist-info → casambi_bt_revamped-0.3.12.dev5.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|