casambi-bt-revamped 0.3.12.dev12__py3-none-any.whl → 0.3.12.dev14__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
@@ -366,6 +366,24 @@ class CasambiClient:
366
366
  f"Classic connection hash read failed/too short (len={0 if raw_hash is None else len(raw_hash)})."
367
367
  )
368
368
  self._classicConnHash8 = bytes(raw_hash[:8])
369
+
370
+ # Parse Android's extended connection hash fields for diagnostics.
371
+ # Offset 8: unitId, 9: flags_lo, 10: MTU, 11: protocolVersion, 12: flags_hi
372
+ if len(raw_hash) >= 13:
373
+ ext_unit_id = raw_hash[8]
374
+ ext_flags_lo = raw_hash[9]
375
+ ext_mtu = raw_hash[10]
376
+ ext_proto_ver = raw_hash[11]
377
+ ext_flags_hi = raw_hash[12]
378
+ self._logger.warning(
379
+ "[CASAMBI_CLASSIC_CONN_HASH_EXT] variant=legacy unitId=%d flags=0x%04x mtu=%d protocolVersion=%d raw=%s",
380
+ ext_unit_id,
381
+ (ext_flags_hi << 8) | ext_flags_lo,
382
+ ext_mtu,
383
+ ext_proto_ver,
384
+ b2a(bytes(raw_hash[:min(len(raw_hash), 20)])),
385
+ )
386
+
369
387
  # Android seeds the command divider with a random byte on startup (u1.C1751c).
370
388
  self._classicCmdDiv = int.from_bytes(os.urandom(1), "big") or 1
371
389
  self._classicTxSeq = 0
@@ -516,6 +534,24 @@ class CasambiClient:
516
534
  self._classicHeaderMode = "conformant"
517
535
  self._classicHashSource = "ca52_0001"
518
536
  self._classicConnHash8 = bytes(first[:8])
537
+
538
+ # Parse Android's extended connection hash fields for diagnostics.
539
+ # Offset 8: unitId, 9: flags_lo, 10: MTU, 11: protocolVersion, 12: flags_hi
540
+ if len(first) >= 13:
541
+ ext_unit_id = first[8]
542
+ ext_flags_lo = first[9]
543
+ ext_mtu = first[10]
544
+ ext_proto_ver = first[11]
545
+ ext_flags_hi = first[12]
546
+ self._logger.warning(
547
+ "[CASAMBI_CLASSIC_CONN_HASH_EXT] variant=conformant unitId=%d flags=0x%04x mtu=%d protocolVersion=%d raw=%s",
548
+ ext_unit_id,
549
+ (ext_flags_hi << 8) | ext_flags_lo,
550
+ ext_mtu,
551
+ ext_proto_ver,
552
+ b2a(bytes(first[:min(len(first), 20)])),
553
+ )
554
+
519
555
  self._classicCmdDiv = int.from_bytes(os.urandom(1), "big") or 1
520
556
  self._classicTxSeq = 0
521
557
 
@@ -1045,11 +1081,11 @@ class CasambiClient:
1045
1081
  else:
1046
1082
  return bytes([counter, unit_id & 0xFF, 1, dimmer & 0xFF])
1047
1083
 
1048
- async def _sendClassic(self, command_bytes: bytes) -> None:
1084
+ async def _sendClassic(self, command_bytes: bytes, *, target_uuid: str | None = None) -> None:
1049
1085
  self._checkState(ConnectionState.AUTHENTICATED)
1050
1086
  if self._protocolMode != ProtocolMode.CLASSIC:
1051
1087
  raise ProtocolError("Classic send called while not in Classic protocol mode.")
1052
- tx_uuid = self._classicTxCharUuid or self._dataCharUuid
1088
+ tx_uuid = target_uuid or self._classicTxCharUuid or self._dataCharUuid
1053
1089
  if not tx_uuid:
1054
1090
  raise ProtocolError("Classic TX characteristic UUID not set.")
1055
1091
  if self._classicConnHash8 is None:
@@ -1304,12 +1340,22 @@ class CasambiClient:
1304
1340
  if offset is not None:
1305
1341
  utc_offset_minutes = int(offset.total_seconds()) // 60
1306
1342
 
1343
+ # Determine time-sync target and command byte per Android AbstractC1717h.X():
1344
+ # - Non-conformant: write to CA51, command byte 10
1345
+ # - Conformant: write to 0002 (mapped CA51), command byte 7
1346
+ if self._classicHeaderMode == "conformant":
1347
+ timesync_uuid = CASA_CLASSIC_CONFORMANT_CA51_CHAR_UUID # 0002
1348
+ timesync_cmd = 7
1349
+ else:
1350
+ timesync_uuid = CASA_CLASSIC_HASH_CHAR_UUID # CA51
1351
+ timesync_cmd = 10
1352
+
1307
1353
  # Build the time-sync payload.
1308
- # Format: [10][year:2BE][month:1][day:1][hour:1][min:1][sec:1]
1354
+ # Format: [cmd][year:2BE][month:1][day:1][hour:1][min:1][sec:1]
1309
1355
  # [tz_offset:2BE signed][dst_transition:4BE][dst_change:1]
1310
1356
  # [timestamp1:3BE][timestamp2:3BE][zero:2][millis:3BE][extra:1]
1311
1357
  payload = bytearray()
1312
- payload.append(10) # Classic time-sync command byte
1358
+ payload.append(timesync_cmd)
1313
1359
  payload.extend(struct.pack(">H", now.year))
1314
1360
  payload.append(now.month)
1315
1361
  payload.append(now.day)
@@ -1337,13 +1383,16 @@ class CasambiClient:
1337
1383
  payload.append((ts1 >> 24) & 0xFF) # writeByte(iK0 >> 24)
1338
1384
 
1339
1385
  self._logger.warning(
1340
- "[CASAMBI_CLASSIC_INIT] sending time-sync len=%d hex=%s",
1386
+ "[CASAMBI_CLASSIC_INIT] sending time-sync len=%d cmd=%d target_uuid=%s header_mode=%s hex=%s",
1341
1387
  len(payload),
1388
+ timesync_cmd,
1389
+ timesync_uuid,
1390
+ self._classicHeaderMode,
1342
1391
  b2a(bytes(payload)),
1343
1392
  )
1344
1393
 
1345
1394
  try:
1346
- await self._sendClassic(bytes(payload))
1395
+ await self._sendClassic(bytes(payload), target_uuid=timesync_uuid)
1347
1396
  self._logger.warning("[CASAMBI_CLASSIC_INIT] time-sync sent successfully")
1348
1397
  except Exception:
1349
1398
  self._logger.warning(
CasambiBt/_network.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import json
2
2
  import logging
3
- import platform
4
3
  import pickle
4
+ import random
5
5
  from dataclasses import dataclass
6
6
  from datetime import datetime, timedelta
7
7
  from typing import Any, Final, cast
@@ -13,7 +13,6 @@ from ._cache import Cache
13
13
  from ._constants import DEVICE_NAME
14
14
  from ._keystore import KeyStore
15
15
  from ._unit import Group, Scene, Unit, UnitControl, UnitControlType, UnitType
16
- from ._version import __version__
17
16
  from .errors import (
18
17
  AuthenticationError,
19
18
  NetworkNotFoundError,
@@ -66,29 +65,49 @@ class Network:
66
65
 
67
66
  self._cache = cache
68
67
 
69
- # Android always includes a "token" (and typically "clientInfo") in cloud requests.
70
- # We keep these stable for the process lifetime to make tester logs comparable.
71
- self._token: str = self._make_token()
72
- self._clientInfo: dict[str, Any] = self._make_client_info()
73
-
74
- @staticmethod
75
- def _make_token() -> str:
76
- # Ground truth: casambi-android `w1.o.p(...)` sends `token` for session requests.
77
- #
78
- # Keep this structured (Android uses "brand/model/device/cpu/unknown") but avoid hostnames/PII.
79
- sys = platform.system().lower() or "unknown"
80
- machine = platform.machine().lower() or "unknown"
81
- return f"python/{sys}/{machine}/unknown/unknown"
82
-
83
- @staticmethod
84
- def _make_client_info() -> dict[str, Any]:
85
- # Ground truth: casambi-android `w1.o.g(...)` includes `clientInfo`.
86
- return {
87
- "name": "casambi-bt-revamped",
88
- "version": __version__,
89
- "python": platform.python_version(),
90
- "platform": platform.platform(),
91
- }
68
+ # Android sends "fb:<FCM_token>" we have no push token, use empty string
69
+ # to match the field type without leaking platform info.
70
+ self._token: str = ""
71
+ # Android: "{flavor}/{version} {manufacturer}_{model}/{os_release}"
72
+ _app_version = random.choice((
73
+ "3.19.0", "3.18.2", "3.18.1", "3.18.0",
74
+ "3.17.4", "3.17.3", "3.17.2", "3.17.1", "3.17.0",
75
+ "3.16.5", "3.16.4", "3.16.3", "3.16.1", "3.16.0",
76
+ "3.15.3", "3.15.2", "3.15.1", "3.15.0",
77
+ "3.14.2", "3.14.1", "3.14.0",
78
+ "3.13.2", "3.13.1", "3.13.0",
79
+ "3.12.4", "3.12.3", "3.12.1", "3.12.0",
80
+ "3.11.2", "3.11.1",
81
+ ))
82
+ _device = random.choice((
83
+ # Samsung Galaxy S series
84
+ "samsung_SM-S928B/15", # S24 Ultra
85
+ "samsung_SM-S926B/15", # S24+
86
+ "samsung_SM-S921B/15", # S24
87
+ "samsung_SM-S918B/14", # S23 Ultra
88
+ "samsung_SM-S916B/14", # S23+
89
+ "samsung_SM-S911B/14", # S23
90
+ "samsung_SM-S908B/14", # S22 Ultra
91
+ "samsung_SM-S906B/14", # S22+
92
+ "samsung_SM-S901B/14", # S22
93
+ "samsung_SM-G998B/13", # S21 Ultra
94
+ "samsung_SM-G996B/13", # S21+
95
+ "samsung_SM-G991B/13", # S21
96
+ # Samsung Galaxy A series
97
+ "samsung_SM-A556B/14", # A55
98
+ "samsung_SM-A546B/14", # A54
99
+ "samsung_SM-A346B/14", # A34
100
+ "samsung_SM-A536B/13", # A53
101
+ # Google Pixel
102
+ "Google_Pixel 8 Pro/14",
103
+ "Google_Pixel 8/14",
104
+ "Google_Pixel 7 Pro/14",
105
+ "Google_Pixel 7/14",
106
+ # OnePlus
107
+ "OnePlus_IN2023/14", # 12
108
+ "OnePlus_CPH2449/14", # 11
109
+ ))
110
+ self._clientInfo: str = f"Casambi/{_app_version} {_device}"
92
111
 
93
112
  async def load(self) -> None:
94
113
  self._keystore = KeyStore(self._cache)
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.dev12"
10
+ __version__ = "0.3.12.dev14"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: casambi-bt-revamped
3
- Version: 0.3.12.dev12
3
+ Version: 0.3.12.dev14
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=dAZZ0S2-t2ShLbW78AE9lOLBzOmhBOTTXJky-6khdkE,41981
4
4
  CasambiBt/_classic_crypto.py,sha256=XIp3JBaeY8hIUv5kB0ygVG_eRx9AgHHF4ts2--CFm78,4973
5
- CasambiBt/_client.py,sha256=a257JmcbYvdP0gSy-W1t3oqunA78dvhXMx-L_TMhr8o,95405
5
+ CasambiBt/_client.py,sha256=lLbLp1rVZjQOgVzbKqQVb9vEkMwwyAH4XwD-0RPLrHU,97684
6
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=ai1o3EybsAhjyPohSOxeE0cWoFvEqdcc3PE3uFDaTfE,21346
11
+ CasambiBt/_network.py,sha256=3ZUedQlHzzuHHiG5KxDLnK0AIz0TjzG1_vwg0UGsO9U,22132
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=BO8IqQymoBlsw14NOEBxAwMuuwVph3QNtmoRlFXHAXI,338
15
+ CasambiBt/_version.py,sha256=aa33jTV294g_KxSqvh0EMBOt5gdfPne-teLPH7wAnIk,338
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.dev12.dist-info/licenses/LICENSE,sha256=TAIIitFxpxEDi6Iju7foW4TDQmWvC-IhLVLhl67jKmQ,11341
19
- casambi_bt_revamped-0.3.12.dev12.dist-info/METADATA,sha256=xjFFDZYZ1zgTbiYSSelRo2BaT2pzSkmTlIBcpOG_qxY,5878
20
- casambi_bt_revamped-0.3.12.dev12.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
21
- casambi_bt_revamped-0.3.12.dev12.dist-info/top_level.txt,sha256=uNbqLjtecFosoFzpGAC89-5icikWODKI8rOjbi8v_sA,10
22
- casambi_bt_revamped-0.3.12.dev12.dist-info/RECORD,,
18
+ casambi_bt_revamped-0.3.12.dev14.dist-info/licenses/LICENSE,sha256=TAIIitFxpxEDi6Iju7foW4TDQmWvC-IhLVLhl67jKmQ,11341
19
+ casambi_bt_revamped-0.3.12.dev14.dist-info/METADATA,sha256=3FFHkPqIBRm6slGUWcvXxT522fyErxH0dP8ywQrjM4E,5878
20
+ casambi_bt_revamped-0.3.12.dev14.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
21
+ casambi_bt_revamped-0.3.12.dev14.dist-info/top_level.txt,sha256=uNbqLjtecFosoFzpGAC89-5icikWODKI8rOjbi8v_sA,10
22
+ casambi_bt_revamped-0.3.12.dev14.dist-info/RECORD,,