casambi-bt-revamped 0.3.12.dev6__py3-none-any.whl → 0.3.12.dev8__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
@@ -142,6 +142,13 @@ class CasambiClient:
142
142
  self._classicRxVerified = 0
143
143
  self._classicRxUnverifiable = 0
144
144
  self._classicRxParseFail = 0
145
+ self._classicRxType6 = 0
146
+ self._classicRxType7 = 0
147
+ self._classicRxType9 = 0
148
+ self._classicRxCmdStream = 0
149
+ self._classicRxUnknown = 0
150
+ # Per-kind sample counters to ensure we emit at least a few examples for reverse engineering.
151
+ self._classicRxKindSamples: dict[str, int] = {}
145
152
  self._classicRxLastStatsTs = time.monotonic()
146
153
 
147
154
  @property
@@ -259,17 +266,20 @@ class CasambiClient:
259
266
  auth_err: str | None = None
260
267
  device_nodeinfo_protocol: int | None = None
261
268
 
262
- def _log_probe_summary(mode: str) -> None:
269
+ def _log_probe_summary(mode: str, *, classic_variant: str | None = None) -> None:
263
270
  # One stable, high-signal line for testers.
264
271
  self._logger.warning(
265
- "[CASAMBI_PROTOCOL_PROBE] address=%s mode=%s cloud_protocol=%s nodeinfo_b1=%s "
266
- "data_uuid=%s classic_hash8_present=%s auth_read_prefix=%s ca51_read_prefix=%s ca51_read_error=%s auth_read_error=%s",
272
+ "[CASAMBI_PROTOCOL_PROBE] address=%s mode=%s cloud_protocol=%s nodeinfo_b1=%s data_uuid=%s "
273
+ "classic_variant=%s ca51_hash8_present=%s conn_hash8_ready=%s "
274
+ "auth_read_prefix=%s ca51_read_prefix=%s ca51_read_error=%s auth_read_error=%s",
267
275
  self.address,
268
276
  mode,
269
277
  cloud_protocol,
270
278
  device_nodeinfo_protocol,
271
279
  self._dataCharUuid,
280
+ classic_variant,
272
281
  bool(classic_hash and len(classic_hash) >= 8),
282
+ self._classicConnHash8 is not None,
273
283
  auth_prefix,
274
284
  ca51_prefix,
275
285
  ca51_err,
@@ -343,7 +353,21 @@ class CasambiClient:
343
353
  len(self._classicConnHash8),
344
354
  b2a(self._classicConnHash8),
345
355
  )
346
- _log_probe_summary("CLASSIC")
356
+ self._logger.warning(
357
+ "[CASAMBI_CLASSIC_SELECTED] address=%s variant=ca52_legacy data_uuid=%s start_notify_uuid=%s header_mode=%s conn_hash8_prefix=%s",
358
+ self.address,
359
+ self._dataCharUuid,
360
+ CASA_CLASSIC_DATA_CHAR_UUID,
361
+ self._classicHeaderMode,
362
+ b2a(self._classicConnHash8),
363
+ )
364
+ self._logger.warning(
365
+ "[CASAMBI_CLASSIC_KEYS] visitor=%s manager=%s cloud_session_is_manager=%s",
366
+ self._network.classicVisitorKey() is not None,
367
+ self._network.classicManagerKey() is not None,
368
+ getattr(self._network, "isManager", lambda: False)(),
369
+ )
370
+ _log_probe_summary("CLASSIC", classic_variant="ca52_legacy")
347
371
  return
348
372
 
349
373
  # Conformant devices can expose the Classic signed channel on the EVO-style UUID too.
@@ -437,7 +461,21 @@ class CasambiClient:
437
461
  len(self._classicConnHash8),
438
462
  b2a(self._classicConnHash8),
439
463
  )
440
- _log_probe_summary("CLASSIC")
464
+ self._logger.warning(
465
+ "[CASAMBI_CLASSIC_SELECTED] address=%s variant=auth_uuid_conformant data_uuid=%s start_notify_uuid=%s header_mode=%s conn_hash8_prefix=%s",
466
+ self.address,
467
+ self._dataCharUuid,
468
+ CASA_AUTH_CHAR_UUID,
469
+ self._classicHeaderMode,
470
+ b2a(self._classicConnHash8),
471
+ )
472
+ self._logger.warning(
473
+ "[CASAMBI_CLASSIC_KEYS] visitor=%s manager=%s cloud_session_is_manager=%s",
474
+ self._network.classicVisitorKey() is not None,
475
+ self._network.classicManagerKey() is not None,
476
+ getattr(self._network, "isManager", lambda: False)(),
477
+ )
478
+ _log_probe_summary("CLASSIC", classic_variant="auth_uuid_conformant")
441
479
  return
442
480
 
443
481
  _log_probe_summary("UNKNOWN")
@@ -832,6 +870,34 @@ class CasambiClient:
832
870
  visitor_key = self._network.classicVisitorKey()
833
871
  manager_key = self._network.classicManagerKey()
834
872
 
873
+ # Parse the command record for logs (u1.C1753e export format).
874
+ cmd_ordinal: int | None = None
875
+ cmd_div: int | None = None
876
+ cmd_target: int | None = None
877
+ cmd_lifetime: int | None = None
878
+ cmd_payload_len: int | None = None
879
+ try:
880
+ if len(command_bytes) >= 2:
881
+ typ = command_bytes[1]
882
+ cmd_ordinal = typ & 0x3F
883
+ has_div = (typ & 0x40) != 0
884
+ has_target = (typ & 0x80) != 0
885
+ p = 2
886
+ if has_div and p < len(command_bytes):
887
+ cmd_div = command_bytes[p]
888
+ p += 1
889
+ if has_target and p < len(command_bytes):
890
+ cmd_target = command_bytes[p]
891
+ p += 1
892
+ if p < len(command_bytes):
893
+ cmd_lifetime = command_bytes[p]
894
+ p += 1
895
+ if p <= len(command_bytes):
896
+ cmd_payload_len = len(command_bytes) - p
897
+ except Exception:
898
+ # If parsing fails, keep fields as None.
899
+ pass
900
+
835
901
  # Key selection mirrors Android's intent:
836
902
  # - Use manager key if our cloud session is manager and a managerKey exists.
837
903
  # - Else use visitor key if present.
@@ -895,17 +961,34 @@ class CasambiClient:
895
961
  else:
896
962
  raise ProtocolError(f"Unknown Classic header mode: {header_mode}")
897
963
 
964
+ signed = key is not None
965
+ if not signed and self._logLimiter.allow("classic_tx_unsigned", burst=10, window_s=300.0):
966
+ self._logger.warning(
967
+ "[CASAMBI_CLASSIC_TX_UNSIGNED] reason=keys_missing visitor=%s manager=%s",
968
+ visitor_key is not None,
969
+ manager_key is not None,
970
+ )
971
+
898
972
  # WARNING-level TX logs are intentional: they are needed for Classic reverse engineering.
899
973
  # Keep payload logging minimal (prefix only).
900
974
  if self._logLimiter.allow("classic_tx", burst=50, window_s=60.0):
975
+ auth_str = f"0x{auth_level:02x}" if header_mode == "conformant" else None
901
976
  self._logger.warning(
902
- "[CASAMBI_CLASSIC_TX] header=%s key=%s auth=0x%02x sig_len=%d seq=%s cmd_len=%d total_len=%d prefix=%s",
977
+ "[CASAMBI_CLASSIC_TX] header=%s key=%s signed=%s auth=%s sig_len=%d seq=%s "
978
+ "cmd_len=%d cmd_ord=%s target=%s div=%s lifetime=%s payload_len=%s "
979
+ "total_len=%d prefix=%s",
903
980
  header_mode,
904
981
  key_name,
905
- auth_level,
982
+ signed,
983
+ auth_str,
906
984
  sig_len,
907
985
  None if seq is None else f"0x{seq:04x}",
908
986
  len(command_bytes),
987
+ cmd_ordinal,
988
+ cmd_target,
989
+ cmd_div,
990
+ cmd_lifetime,
991
+ cmd_payload_len,
909
992
  len(pkt),
910
993
  b2a(bytes(pkt[: min(len(pkt), 24)])),
911
994
  )
@@ -1213,17 +1296,47 @@ class CasambiClient:
1213
1296
  ):
1214
1297
  self._classicRxLastStatsTs = now
1215
1298
  self._logger.warning(
1216
- "[CASAMBI_CLASSIC_RX_STATS] frames=%d verified=%d unverifiable=%d parse_fail=%d header=%s",
1299
+ "[CASAMBI_CLASSIC_RX_STATS] frames=%d verified=%d unverifiable=%d parse_fail=%d header=%s "
1300
+ "type6=%d type7=%d type9=%d cmdstream=%d unknown=%d",
1217
1301
  self._classicRxFrames,
1218
1302
  self._classicRxVerified,
1219
1303
  self._classicRxUnverifiable,
1220
1304
  self._classicRxParseFail,
1221
1305
  self._classicHeaderMode,
1306
+ self._classicRxType6,
1307
+ self._classicRxType7,
1308
+ self._classicRxType9,
1309
+ self._classicRxCmdStream,
1310
+ self._classicRxUnknown,
1222
1311
  )
1223
1312
 
1224
1313
  # If the payload starts with a known EVO packet type, reuse existing parsers.
1225
1314
  packet_type = payload[0]
1226
1315
  if packet_type in (IncommingPacketType.UnitState, IncommingPacketType.SwitchEvent, IncommingPacketType.NetworkConfig):
1316
+ kind = f"type{int(packet_type)}"
1317
+ if packet_type == IncommingPacketType.UnitState:
1318
+ self._classicRxType6 += 1
1319
+ kind = "type6_unitstate"
1320
+ elif packet_type == IncommingPacketType.SwitchEvent:
1321
+ self._classicRxType7 += 1
1322
+ kind = "type7_switch"
1323
+ else:
1324
+ self._classicRxType9 += 1
1325
+ kind = "type9_netconf"
1326
+
1327
+ # Emit a few per-kind examples for reverse engineering.
1328
+ if self._classicRxKindSamples.get(kind, 0) < 3:
1329
+ self._classicRxKindSamples[kind] = self._classicRxKindSamples.get(kind, 0) + 1
1330
+ self._logger.warning(
1331
+ "[CASAMBI_CLASSIC_RX_KIND] kind=%s header=%s verified=%s sig_len=%d seq=%s payload_prefix=%s",
1332
+ kind,
1333
+ best["mode"],
1334
+ verified,
1335
+ best["sig_len"],
1336
+ None if best["seq"] is None else f"0x{best['seq']:04x}",
1337
+ b2a(payload[: min(len(payload), 32)]),
1338
+ )
1339
+
1227
1340
  if self._logger.isEnabledFor(logging.DEBUG):
1228
1341
  self._logger.debug(
1229
1342
  "[CASAMBI_CLASSIC_RX_PAYLOAD] type=%d len=%d hex=%s",
@@ -1244,6 +1357,7 @@ class CasambiClient:
1244
1357
  # Otherwise, attempt to parse a stream of Classic "command" records:
1245
1358
  # record[0] = (len + 239) mod 256, so len = (b0 - 239) & 0xFF.
1246
1359
  pos = 0
1360
+ parsed_any = False
1247
1361
  while pos + 2 <= len(payload):
1248
1362
  enc_len = payload[pos]
1249
1363
  rec_len = (enc_len - 239) & 0xFF
@@ -1251,6 +1365,7 @@ class CasambiClient:
1251
1365
  break
1252
1366
  rec = payload[pos : pos + rec_len]
1253
1367
  pos += rec_len
1368
+ parsed_any = True
1254
1369
 
1255
1370
  typ = rec[1]
1256
1371
  ordinal = typ & 0x3F
@@ -1278,6 +1393,25 @@ class CasambiClient:
1278
1393
  b2a(rec_payload),
1279
1394
  )
1280
1395
 
1396
+ if parsed_any:
1397
+ self._classicRxCmdStream += 1
1398
+ kind = "cmdstream"
1399
+ else:
1400
+ self._classicRxUnknown += 1
1401
+ kind = "unknown"
1402
+
1403
+ if self._classicRxKindSamples.get(kind, 0) < 3:
1404
+ self._classicRxKindSamples[kind] = self._classicRxKindSamples.get(kind, 0) + 1
1405
+ self._logger.warning(
1406
+ "[CASAMBI_CLASSIC_RX_KIND] kind=%s header=%s verified=%s sig_len=%d seq=%s payload_prefix=%s",
1407
+ kind,
1408
+ best["mode"],
1409
+ verified,
1410
+ best["sig_len"],
1411
+ None if best["seq"] is None else f"0x{best['seq']:04x}",
1412
+ b2a(payload[: min(len(payload), 32)]),
1413
+ )
1414
+
1281
1415
  # Any trailing bytes that don't form a full record are logged for analysis.
1282
1416
  if self._logger.isEnabledFor(logging.DEBUG) and pos < len(payload):
1283
1417
  self._logger.debug(
CasambiBt/_network.py CHANGED
@@ -262,16 +262,16 @@ class Network:
262
262
  getNetworkUrl = f"https://api.casambi.com/network/{self._id}/"
263
263
 
264
264
  try:
265
+ payload = {
266
+ "formatVersion": 1,
267
+ "deviceName": DEVICE_NAME,
268
+ "revision": self._networkRevision,
269
+ }
270
+
265
271
  # **SECURITY**: Do not set session header for client! This could leak the session with external clients.
266
272
  res = await self._httpClient.put(
267
273
  getNetworkUrl,
268
- json={
269
- "formatVersion": 1,
270
- "token": self._token,
271
- "deviceName": DEVICE_NAME,
272
- "clientInfo": self._clientInfo,
273
- "revision": self._networkRevision,
274
- },
274
+ json=payload,
275
275
  headers={"X-Casambi-Session": self._session.session}, # type: ignore[union-attr]
276
276
  )
277
277
 
@@ -284,8 +284,28 @@ class Network:
284
284
  )
285
285
  await self._cache.invalidateCache()
286
286
 
287
+ if res.status_code == httpx.codes.BAD_REQUEST:
288
+ # Some backend variants may reject the minimal update payload.
289
+ # Retry once with Android-like fields (token/clientInfo) for diagnostics/testing.
290
+ self._logger.warning(
291
+ "[CASAMBI_CLOUD_UPDATE_RETRY] status=400 retry_with_token_clientInfo=true body_prefix=%r",
292
+ (res.text or "")[:200],
293
+ )
294
+ payload2 = dict(payload)
295
+ payload2["token"] = self._token
296
+ payload2["clientInfo"] = self._clientInfo
297
+ res = await self._httpClient.put(
298
+ getNetworkUrl,
299
+ json=payload2,
300
+ headers={"X-Casambi-Session": self._session.session}, # type: ignore[union-attr]
301
+ )
302
+
287
303
  if res.status_code != httpx.codes.OK:
288
- self._logger.error(f"Update failed: {res.status_code}\n{res.text}")
304
+ self._logger.error(
305
+ "Update failed: %s body_prefix=%r",
306
+ res.status_code,
307
+ (res.text or "")[:500],
308
+ )
289
309
  raise NetworkUpdateError("Could not update network!")
290
310
 
291
311
  self._logger.debug(f"Network: {res.text}")
CasambiBt/_version.py CHANGED
@@ -7,4 +7,4 @@ Avoid using importlib.metadata in hot paths by providing a static version string
7
7
  __all__ = ["__version__"]
8
8
 
9
9
  # NOTE: Must match `casambi-bt/setup.cfg` [metadata] version.
10
- __version__ = "0.3.12.dev6"
10
+ __version__ = "0.3.12.dev8"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: casambi-bt-revamped
3
- Version: 0.3.12.dev6
3
+ Version: 0.3.12.dev8
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,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=PNYBwMdehh-YvSdxf8I-74bpn008VjNvwZyru5H_LuM,63618
5
+ CasambiBt/_client.py,sha256=dG-VRlZ0n7Eng8ORc-Xk8rifCVAcXBexFroA4BLQ_w8,69657
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
9
9
  CasambiBt/_invocation.py,sha256=fkG4R0Gv5_amFfD_P6DKuIEe3oKWZW0v8RSU8zDjPdI,2985
10
10
  CasambiBt/_keystore.py,sha256=Jdiq0zMPDmhfpheSojKY6sTUpmVrvX_qOyO7yCYd3kw,2788
11
- CasambiBt/_network.py,sha256=UMGpB-seAXtfwPS7pvXTieLf9ekFXgvy57tAfcc_cno,19779
11
+ CasambiBt/_network.py,sha256=nB_pRB9dZL6P7THeuOce7ctWd0wXyCWF13h67SauZVQ,20714
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=spRApATilqicOYCOi-3PEHxfpK9lOYP1fW1ufdiSN5Q,337
15
+ CasambiBt/_version.py,sha256=RkpM6Fp6uH7xKTYzqUnnINOKTs0TrFqLrkU4nloEFrU,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.dev6.dist-info/licenses/LICENSE,sha256=TAIIitFxpxEDi6Iju7foW4TDQmWvC-IhLVLhl67jKmQ,11341
19
- casambi_bt_revamped-0.3.12.dev6.dist-info/METADATA,sha256=mwWxQMdafeUx5y1uwhn-n_CegEkuzS9uJ4hODJJ0RI8,5877
20
- casambi_bt_revamped-0.3.12.dev6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
21
- casambi_bt_revamped-0.3.12.dev6.dist-info/top_level.txt,sha256=uNbqLjtecFosoFzpGAC89-5icikWODKI8rOjbi8v_sA,10
22
- casambi_bt_revamped-0.3.12.dev6.dist-info/RECORD,,
18
+ casambi_bt_revamped-0.3.12.dev8.dist-info/licenses/LICENSE,sha256=TAIIitFxpxEDi6Iju7foW4TDQmWvC-IhLVLhl67jKmQ,11341
19
+ casambi_bt_revamped-0.3.12.dev8.dist-info/METADATA,sha256=d0oJkqNgiNr_ACzBbE_6Z2i93Wsa1oG_gZi54xgNiJo,5877
20
+ casambi_bt_revamped-0.3.12.dev8.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
21
+ casambi_bt_revamped-0.3.12.dev8.dist-info/top_level.txt,sha256=uNbqLjtecFosoFzpGAC89-5icikWODKI8rOjbi8v_sA,10
22
+ casambi_bt_revamped-0.3.12.dev8.dist-info/RECORD,,