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 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
- raise UnsupportedProtocolVersion(
125
- f"Legacy version aren't supported currently. Your network version is {version}. Minimum version is {MIN_VERSION}."
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. Your network version is %i. Highest supported version is %i. Continue at your own risk.",
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
- except Exception:
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.debug("Classic CA52 notify failed; trying auth UUID probing.", exc_info=True)
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
- except Exception:
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(self._network.protocolVersion)
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.debug(f"Got {b2a(firstResp)}")
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
- # Check type and protocol version
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
- "Unexpected answer from device! Wrong device or protocol version? Trying to continue."
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(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: casambi-bt-revamped
3
- Version: 0.3.12.dev4
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
@@ -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=yTSuAeJhBXp5Zs3jU-RvHFEpI-quRNwlB3HWGl7q_yY,50730
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.dev4.dist-info/licenses/LICENSE,sha256=TAIIitFxpxEDi6Iju7foW4TDQmWvC-IhLVLhl67jKmQ,11341
18
- casambi_bt_revamped-0.3.12.dev4.dist-info/METADATA,sha256=DKE1xb6Jg8lORTpoWyiM8qaSBOXOb5V_l7phDqWHGBA,5877
19
- casambi_bt_revamped-0.3.12.dev4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
20
- casambi_bt_revamped-0.3.12.dev4.dist-info/top_level.txt,sha256=uNbqLjtecFosoFzpGAC89-5icikWODKI8rOjbi8v_sA,10
21
- casambi_bt_revamped-0.3.12.dev4.dist-info/RECORD,,
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,,