casambi-bt-revamped 0.3.12.dev11__py3-none-any.whl → 0.3.12.dev13__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
@@ -438,6 +438,7 @@ class CasambiClient:
438
438
  self._network.classicManagerKey() is not None,
439
439
  getattr(self._network, "isManager", lambda: False)(),
440
440
  )
441
+ await self._classicEnumerateAndSubscribeGatt(notify_kwargs)
441
442
  _log_probe_summary("CLASSIC", classic_variant="ca52_legacy")
442
443
  # Emit a warning if we never see Classic RX frames; this is a common failure mode.
443
444
  self._classicNoRxTask = asyncio.create_task(self._classic_no_rx_watchdog(30.0))
@@ -593,6 +594,7 @@ class CasambiClient:
593
594
  self._network.classicManagerKey() is not None,
594
595
  getattr(self._network, "isManager", lambda: False)(),
595
596
  )
597
+ await self._classicEnumerateAndSubscribeGatt(notify_kwargs)
596
598
  _log_probe_summary("CLASSIC", classic_variant="auth_uuid_conformant")
597
599
  self._classicNoRxTask = asyncio.create_task(self._classic_no_rx_watchdog(30.0))
598
600
  return
@@ -1223,6 +1225,58 @@ class CasambiClient:
1223
1225
  len(pkt),
1224
1226
  )
1225
1227
 
1228
+ async def _classicEnumerateAndSubscribeGatt(
1229
+ self, notify_kwargs: dict[str, Any]
1230
+ ) -> None:
1231
+ """Enumerate all GATT characteristics and subscribe to any notifiable ones.
1232
+
1233
+ This discovers characteristics beyond the manually-probed CA51/CA52/CA53
1234
+ UUIDs and subscribes to any that support notify or indicate, which may be
1235
+ needed for receiving Classic state/config notifications.
1236
+ """
1237
+ try:
1238
+ total_chars = 0
1239
+ for svc in self._gattClient.services:
1240
+ for char in svc.characteristics:
1241
+ total_chars += 1
1242
+ char_uuid = str(char.uuid).lower()
1243
+ props = char.properties
1244
+ self._logger.warning(
1245
+ "[CASAMBI_CLASSIC_GATT_CHAR] uuid=%s props=%s handle=%d",
1246
+ char_uuid,
1247
+ props,
1248
+ char.handle,
1249
+ )
1250
+ if char_uuid not in self._classicNotifyCharUuids:
1251
+ if "notify" in props or "indicate" in props:
1252
+ try:
1253
+ await self._gattClient.start_notify(
1254
+ char.uuid,
1255
+ self._queueCallback,
1256
+ **notify_kwargs,
1257
+ )
1258
+ self._classicNotifyCharUuids.add(char_uuid)
1259
+ self._logger.warning(
1260
+ "[CASAMBI_CLASSIC_GATT_SUB] subscribed uuid=%s",
1261
+ char_uuid,
1262
+ )
1263
+ except Exception as e:
1264
+ self._logger.warning(
1265
+ "[CASAMBI_CLASSIC_GATT_SUB] failed uuid=%s err=%s",
1266
+ char_uuid,
1267
+ type(e).__name__,
1268
+ )
1269
+ self._logger.warning(
1270
+ "[CASAMBI_CLASSIC_GATT_ENUM] total_chars=%d subscribed_uuids=%s",
1271
+ total_chars,
1272
+ sorted(self._classicNotifyCharUuids),
1273
+ )
1274
+ except Exception as e:
1275
+ self._logger.warning(
1276
+ "[CASAMBI_CLASSIC_GATT_ENUM] services enumeration unavailable: %s",
1277
+ type(e).__name__,
1278
+ )
1279
+
1226
1280
  async def classicSendInit(self) -> None:
1227
1281
  """Send Classic post-connection initialization (time-sync).
1228
1282
 
@@ -1253,7 +1307,7 @@ class CasambiClient:
1253
1307
  # Build the time-sync payload.
1254
1308
  # Format: [10][year:2BE][month:1][day:1][hour:1][min:1][sec:1]
1255
1309
  # [tz_offset:2BE signed][dst_transition:4BE][dst_change:1]
1256
- # [timestamp1:4BE][timestamp2:4BE][zero:2][millis:3BE]
1310
+ # [timestamp1:3BE][timestamp2:3BE][zero:2][millis:3BE][extra:1]
1257
1311
  payload = bytearray()
1258
1312
  payload.append(10) # Classic time-sync command byte
1259
1313
  payload.extend(struct.pack(">H", now.year))
@@ -1266,16 +1320,21 @@ class CasambiClient:
1266
1320
  # DST transition data and change minutes (0 = no DST info).
1267
1321
  payload.extend(struct.pack(">I", 0))
1268
1322
  payload.append(0)
1269
- # Classic extra bytes: timestamps, zero short, millis.
1270
- # Start with zeros - refine after tester feedback if needed.
1271
- payload.extend(struct.pack(">I", 0)) # timestamp1
1272
- payload.extend(struct.pack(">I", 0)) # timestamp2
1273
- payload.extend(struct.pack(">H", 0)) # zero
1274
- # j() in Android is a 3-byte big-endian write.
1323
+ # Classic extra bytes: timestamps, zero short, millis, trailing byte.
1324
+ # Android AbstractC1717h.X() lines 323-328: j() = 3-byte big-endian write
1325
+ # (Q2.t.java:59-63), NOT 4-byte. Plus trailing writeByte(iK0 >> 24).
1326
+ ts1 = 0 # Q2.r.K0(network.V) — start with 0
1327
+ ts2 = 0 # Q2.r.K0(network.W) — start with 0
1328
+ for ts in (ts1, ts2):
1329
+ payload.append((ts >> 16) & 0xFF)
1330
+ payload.append((ts >> 8) & 0xFF)
1331
+ payload.append(ts & 0xFF)
1332
+ payload.extend(struct.pack(">H", 0)) # writeShort(0)
1275
1333
  millis_val = now.microsecond // 1000 * 1000
1276
1334
  payload.append((millis_val >> 16) & 0xFF)
1277
1335
  payload.append((millis_val >> 8) & 0xFF)
1278
1336
  payload.append(millis_val & 0xFF)
1337
+ payload.append((ts1 >> 24) & 0xFF) # writeByte(iK0 >> 24)
1279
1338
 
1280
1339
  self._logger.warning(
1281
1340
  "[CASAMBI_CLASSIC_INIT] sending time-sync len=%d hex=%s",
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.dev10"
10
+ __version__ = "0.3.12.dev13"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: casambi-bt-revamped
3
- Version: 0.3.12.dev11
3
+ Version: 0.3.12.dev13
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=-C1WfIa8IOTB5R9-nGSKJwE6yLtJC5Meo5FKUq9_XEw,92546
5
+ CasambiBt/_client.py,sha256=a257JmcbYvdP0gSy-W1t3oqunA78dvhXMx-L_TMhr8o,95405
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=KfDHVZ0HvUoCJCQD90I4l0PCSgOKne4pUVo8Y_Hv5Xk,338
15
+ CasambiBt/_version.py,sha256=kFYpR3yK4UmQAWNKE7Jrv9E0CGPotgTEUKIRF5GnzU8,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.dev11.dist-info/licenses/LICENSE,sha256=TAIIitFxpxEDi6Iju7foW4TDQmWvC-IhLVLhl67jKmQ,11341
19
- casambi_bt_revamped-0.3.12.dev11.dist-info/METADATA,sha256=fTNunmvIZDbzGByz_OJPXkDf2Jstu7y2Rp5DCxZ62IY,5878
20
- casambi_bt_revamped-0.3.12.dev11.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
21
- casambi_bt_revamped-0.3.12.dev11.dist-info/top_level.txt,sha256=uNbqLjtecFosoFzpGAC89-5icikWODKI8rOjbi8v_sA,10
22
- casambi_bt_revamped-0.3.12.dev11.dist-info/RECORD,,
18
+ casambi_bt_revamped-0.3.12.dev13.dist-info/licenses/LICENSE,sha256=TAIIitFxpxEDi6Iju7foW4TDQmWvC-IhLVLhl67jKmQ,11341
19
+ casambi_bt_revamped-0.3.12.dev13.dist-info/METADATA,sha256=9LeLoHYkt4Hp8qzq6MzABk_ww_369Gmky5-FZtEKhe0,5878
20
+ casambi_bt_revamped-0.3.12.dev13.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
21
+ casambi_bt_revamped-0.3.12.dev13.dist-info/top_level.txt,sha256=uNbqLjtecFosoFzpGAC89-5icikWODKI8rOjbi8v_sA,10
22
+ casambi_bt_revamped-0.3.12.dev13.dist-info/RECORD,,