casambi-bt-revamped 0.3.12.dev14__tar.gz → 0.3.12.dev15__tar.gz

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.
Files changed (31) hide show
  1. {casambi_bt_revamped-0.3.12.dev14/src/casambi_bt_revamped.egg-info → casambi_bt_revamped-0.3.12.dev15}/PKG-INFO +1 -1
  2. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/setup.cfg +1 -1
  3. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_client.py +191 -51
  4. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_version.py +1 -1
  5. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15/src/casambi_bt_revamped.egg-info}/PKG-INFO +1 -1
  6. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/LICENSE +0 -0
  7. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/README.md +0 -0
  8. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/pyproject.toml +0 -0
  9. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/__init__.py +0 -0
  10. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_cache.py +0 -0
  11. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_casambi.py +0 -0
  12. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_classic_crypto.py +0 -0
  13. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_constants.py +0 -0
  14. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_discover.py +0 -0
  15. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_encryption.py +0 -0
  16. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_invocation.py +0 -0
  17. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_keystore.py +0 -0
  18. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_network.py +0 -0
  19. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_operation.py +0 -0
  20. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_switch_events.py +0 -0
  21. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_unit.py +0 -0
  22. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/errors.py +0 -0
  23. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/py.typed +0 -0
  24. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/casambi_bt_revamped.egg-info/SOURCES.txt +0 -0
  25. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/casambi_bt_revamped.egg-info/dependency_links.txt +0 -0
  26. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/casambi_bt_revamped.egg-info/requires.txt +0 -0
  27. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/casambi_bt_revamped.egg-info/top_level.txt +0 -0
  28. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/tests/test_classic_protocol.py +0 -0
  29. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/tests/test_legacy_protocol_handling.py +0 -0
  30. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/tests/test_switch_event_logs.py +0 -0
  31. {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/tests/test_unit_state_logs.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: casambi-bt-revamped
3
- Version: 0.3.12.dev14
3
+ Version: 0.3.12.dev15
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
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = casambi-bt-revamped
3
- version = 0.3.12.dev14
3
+ version = 0.3.12.dev15
4
4
  author = rankjie
5
5
  author_email = rankjie@gmail.com
6
6
  description = Forked Casambi Bluetooth client library with switch event support, use original if no special need. https://github.com/lkempf/casambi-bt
@@ -9,7 +9,7 @@ from binascii import b2a_hex as b2a
9
9
  from collections.abc import Callable
10
10
  from enum import Enum, IntEnum, auto, unique
11
11
  from hashlib import sha256
12
- from typing import Any, Final
12
+ from typing import Any, Final, Literal
13
13
 
14
14
  from bleak import BleakClient
15
15
  from bleak.backends.characteristic import BleakGATTCharacteristic
@@ -800,6 +800,11 @@ class CasambiClient:
800
800
  await self._activityLock.acquire()
801
801
  try:
802
802
  self._callbackMulitplexer(handle, data)
803
+ except Exception:
804
+ self._logger.warning(
805
+ "[CASAMBI_CALLBACK_ERROR] unhandled exception in callback multiplexer",
806
+ exc_info=True,
807
+ )
803
808
  finally:
804
809
  self._callbackQueue.task_done()
805
810
  self._activityLock.release()
@@ -811,6 +816,10 @@ class CasambiClient:
811
816
  def _callbackMulitplexer(
812
817
  self, handle: BleakGATTCharacteristic, data: bytes
813
818
  ) -> None:
819
+ if self._logger.isEnabledFor(logging.DEBUG):
820
+ self._logger.debug(
821
+ "[CASAMBI_MUX] state=%s len=%d", self._connectionState, len(data)
822
+ )
814
823
  if self._connectionState == ConnectionState.CONNECTED:
815
824
  self._exchNofityCallback(handle, data)
816
825
  elif self._connectionState == ConnectionState.KEY_EXCHANGED:
@@ -1081,7 +1090,14 @@ class CasambiClient:
1081
1090
  else:
1082
1091
  return bytes([counter, unit_id & 0xFF, 1, dimmer & 0xFF])
1083
1092
 
1084
- async def _sendClassic(self, command_bytes: bytes, *, target_uuid: str | None = None) -> None:
1093
+ async def _sendClassic(
1094
+ self,
1095
+ command_bytes: bytes,
1096
+ *,
1097
+ target_uuid: str | None = None,
1098
+ key_preference: Literal["auto", "visitor", "manager"] = "auto",
1099
+ response: bool | None = None,
1100
+ ) -> None:
1085
1101
  self._checkState(ConnectionState.AUTHENTICATED)
1086
1102
  if self._protocolMode != ProtocolMode.CLASSIC:
1087
1103
  raise ProtocolError("Classic send called while not in Classic protocol mode.")
@@ -1122,26 +1138,41 @@ class CasambiClient:
1122
1138
  # If parsing fails, keep fields as None.
1123
1139
  pass
1124
1140
 
1125
- # Key selection mirrors Android's intent:
1126
- # - Use manager key if our cloud session is manager and a managerKey exists.
1127
- # - Else use visitor key if present.
1128
- # - Else fall back to manager key if present.
1129
- # - Else send an unsigned frame (signature bytes remain zeros), which Android does when keys are null.
1141
+ # Classic key selection:
1142
+ #
1143
+ # Android (v3.16) explicitly uses "visitor" signing (auth_level=2 / 4-byte sig)
1144
+ # for the Classic enable-notify bootstrap packets (sendVersion + sendTime), even
1145
+ # when a managerKey exists.
1146
+ #
1147
+ # For normal commands we keep the historical behavior ("auto" == prefer manager
1148
+ # when the cloud session is manager), but allow overrides so init can match Android.
1130
1149
  key_name = "none"
1131
- auth_level = 0x02 # visitor by default
1132
- key = None
1133
- if manager_key is not None and getattr(self._network, "isManager", lambda: False)():
1134
- key_name = "manager"
1135
- auth_level = 0x03
1136
- key = manager_key
1137
- elif visitor_key is not None:
1138
- key_name = "visitor"
1139
- auth_level = 0x02
1140
- key = visitor_key
1141
- elif manager_key is not None:
1142
- key_name = "manager"
1143
- auth_level = 0x03
1144
- key = manager_key
1150
+ auth_level = 0x02
1151
+ key: bytes | None = None
1152
+
1153
+ if key_preference == "visitor":
1154
+ if visitor_key is not None:
1155
+ key_name, auth_level, key = "visitor", 0x02, visitor_key
1156
+ elif manager_key is not None:
1157
+ # Fallback: some networks have managerKey only.
1158
+ key_name, auth_level, key = "manager", 0x03, manager_key
1159
+ else:
1160
+ key_name, auth_level, key = "none", 0x02, None
1161
+ elif key_preference == "manager":
1162
+ if manager_key is not None:
1163
+ key_name, auth_level, key = "manager", 0x03, manager_key
1164
+ elif visitor_key is not None:
1165
+ key_name, auth_level, key = "visitor", 0x02, visitor_key
1166
+ else:
1167
+ key_name, auth_level, key = "none", 0x03, None
1168
+ else:
1169
+ # "auto" (legacy behavior)
1170
+ if manager_key is not None and getattr(self._network, "isManager", lambda: False)():
1171
+ key_name, auth_level, key = "manager", 0x03, manager_key
1172
+ elif visitor_key is not None:
1173
+ key_name, auth_level, key = "visitor", 0x02, visitor_key
1174
+ elif manager_key is not None:
1175
+ key_name, auth_level, key = "manager", 0x03, manager_key
1145
1176
 
1146
1177
  header_mode = self._classicHeaderMode or "conformant"
1147
1178
 
@@ -1218,11 +1249,14 @@ class CasambiClient:
1218
1249
  b2a(bytes(pkt[: min(len(pkt), 24)])),
1219
1250
  )
1220
1251
 
1221
- # Classic packets can exceed 20 bytes when using a 16-byte manager signature.
1222
- # Bleak needs a write-with-response for long writes on most backends.
1252
+ # Android uses WRITE_TYPE_NO_RESPONSE (1) for version/state writes (n0 path)
1253
+ # and WRITE_TYPE_DEFAULT (2) = with-response for time-sync (X path).
1254
+ # If caller didn't specify, default to True for backward compatibility
1255
+ # (also needed for long writes with 16-byte manager signature).
1256
+ use_response = response if response is not None else True
1223
1257
  tx_result = "pending"
1224
1258
  try:
1225
- await self._gattClient.write_gatt_char(tx_uuid, bytes(pkt), response=True)
1259
+ await self._gattClient.write_gatt_char(tx_uuid, bytes(pkt), response=use_response)
1226
1260
  tx_result = "ok"
1227
1261
  except Exception as e:
1228
1262
  tx_result = f"error: {type(e).__name__}: {e}"
@@ -1314,13 +1348,17 @@ class CasambiClient:
1314
1348
  )
1315
1349
 
1316
1350
  async def classicSendInit(self) -> None:
1317
- """Send Classic post-connection initialization (time-sync).
1351
+ """Send Classic post-connection initialization (version + time-sync).
1352
+
1353
+ Ground truth (casambi-android v3.16):
1354
+ - Enable notify on CA52/0001 (CCCD) (handled by Bleak start_notify)
1355
+ - Send "version" on CA52/0001: bytes [0,1,11]
1356
+ (`Z0/AbstractC0151u.h0()` in Android)
1357
+ - Then send time-sync on CA51/0002 (cmd 10 legacy / 7 conformant)
1358
+ (`Z0/AbstractC0142k.X()` in Android)
1318
1359
 
1319
- Ground truth: casambi-android AbstractC1717h.X() (lines 254-345).
1320
- The Android app sends this as the first packet after Classic connection.
1321
- In EVO, the key exchange/auth handshake implicitly signals the device;
1322
- Classic has no such handshake, so an explicit init write is needed to
1323
- trigger the device to start broadcasting state notifications.
1360
+ Classic often stays silent until this bootstrap is completed, so we do it
1361
+ right after the BLE connection is established.
1324
1362
 
1325
1363
  The payload is sent raw via _sendClassic (NOT wrapped in buildClassicCommand).
1326
1364
  """
@@ -1328,6 +1366,33 @@ class CasambiClient:
1328
1366
  if self._protocolMode != ProtocolMode.CLASSIC:
1329
1367
  return
1330
1368
 
1369
+ # Ensure notify setup has a moment to settle (Android delays ~100ms after CCCD write).
1370
+ await asyncio.sleep(0.1)
1371
+
1372
+ # 1) Send Classic "version" packet on CA52/0001.
1373
+ version_uuid = self._classicTxCharUuid or self._dataCharUuid
1374
+ if version_uuid:
1375
+ try:
1376
+ self._logger.warning(
1377
+ "[CASAMBI_CLASSIC_INIT] sending version len=3 target_uuid=%s header_mode=%s",
1378
+ version_uuid,
1379
+ self._classicHeaderMode,
1380
+ )
1381
+ await self._sendClassic(
1382
+ b"\x00\x01\x0b",
1383
+ target_uuid=version_uuid,
1384
+ # Android uses visitor auth for this bootstrap packet.
1385
+ key_preference="visitor",
1386
+ # Android n0():998 uses WRITE_TYPE_NO_RESPONSE for classic.
1387
+ response=False,
1388
+ )
1389
+ self._logger.warning("[CASAMBI_CLASSIC_INIT] version sent successfully")
1390
+ except Exception:
1391
+ self._logger.warning(
1392
+ "[CASAMBI_CLASSIC_INIT] version send failed (continuing with time-sync)",
1393
+ exc_info=True,
1394
+ )
1395
+
1331
1396
  import datetime as _dt
1332
1397
 
1333
1398
  now = _dt.datetime.now()
@@ -1366,21 +1431,42 @@ class CasambiClient:
1366
1431
  # DST transition data and change minutes (0 = no DST info).
1367
1432
  payload.extend(struct.pack(">I", 0))
1368
1433
  payload.append(0)
1369
- # Classic extra bytes: timestamps, zero short, millis, trailing byte.
1434
+ # Classic extra bytes: lon/lat (fixed-point), zero short, millis, trailing lon high byte.
1370
1435
  # Android AbstractC1717h.X() lines 323-328: j() = 3-byte big-endian write
1371
1436
  # (Q2.t.java:59-63), NOT 4-byte. Plus trailing writeByte(iK0 >> 24).
1372
- ts1 = 0 # Q2.r.K0(network.V) — start with 0
1373
- ts2 = 0 # Q2.r.K0(network.W) start with 0
1374
- for ts in (ts1, ts2):
1375
- payload.append((ts >> 16) & 0xFF)
1376
- payload.append((ts >> 8) & 0xFF)
1377
- payload.append(ts & 0xFF)
1437
+ #
1438
+ # In casambi-android v3.16 these values are derived from:
1439
+ # - longitude: round(longitude * 65536)
1440
+ # - latitude: round(latitude * 65536)
1441
+ # and sent as:
1442
+ # j(lon32) + j(lat32) + ... + writeByte(lon32 >> 24)
1443
+ lon_fp32 = 0
1444
+ lat_fp32 = 0
1445
+ try:
1446
+ raw = getattr(self._network, "rawNetworkData", None)
1447
+ net = raw.get("network") if isinstance(raw, dict) else None
1448
+ if isinstance(net, dict):
1449
+ lon = net.get("longitude")
1450
+ lat = net.get("latitude")
1451
+ if isinstance(lon, (int, float, str)):
1452
+ lon_fp32 = int(round(float(lon) * 65536.0))
1453
+ if isinstance(lat, (int, float, str)):
1454
+ lat_fp32 = int(round(float(lat) * 65536.0))
1455
+ except Exception:
1456
+ # Never fail init due to missing location; send zeros.
1457
+ lon_fp32 = 0
1458
+ lat_fp32 = 0
1459
+
1460
+ for v in (lon_fp32, lat_fp32):
1461
+ payload.append((v >> 16) & 0xFF)
1462
+ payload.append((v >> 8) & 0xFF)
1463
+ payload.append(v & 0xFF)
1378
1464
  payload.extend(struct.pack(">H", 0)) # writeShort(0)
1379
1465
  millis_val = now.microsecond // 1000 * 1000
1380
1466
  payload.append((millis_val >> 16) & 0xFF)
1381
1467
  payload.append((millis_val >> 8) & 0xFF)
1382
1468
  payload.append(millis_val & 0xFF)
1383
- payload.append((ts1 >> 24) & 0xFF) # writeByte(iK0 >> 24)
1469
+ payload.append((lon_fp32 >> 24) & 0xFF) # writeByte(lon >> 24)
1384
1470
 
1385
1471
  self._logger.warning(
1386
1472
  "[CASAMBI_CLASSIC_INIT] sending time-sync len=%d cmd=%d target_uuid=%s header_mode=%s hex=%s",
@@ -1392,7 +1478,14 @@ class CasambiClient:
1392
1478
  )
1393
1479
 
1394
1480
  try:
1395
- await self._sendClassic(bytes(payload), target_uuid=timesync_uuid)
1481
+ await self._sendClassic(
1482
+ bytes(payload),
1483
+ target_uuid=timesync_uuid,
1484
+ # Android uses visitor auth for this bootstrap packet.
1485
+ key_preference="visitor",
1486
+ # Android X():314 uses WRITE_TYPE_DEFAULT (2) = with-response.
1487
+ response=True,
1488
+ )
1396
1489
  self._logger.warning("[CASAMBI_CLASSIC_INIT] time-sync sent successfully")
1397
1490
  except Exception:
1398
1491
  self._logger.warning(
@@ -1410,9 +1503,19 @@ class CasambiClient:
1410
1503
  except Exception:
1411
1504
  handle_uuid = ""
1412
1505
  if handle_uuid and handle_uuid in self._classicNotifyCharUuids:
1506
+ self._logger.debug(
1507
+ "[CASAMBI_NOTIFY_ROUTE] classic_by_uuid uuid=%s len=%d",
1508
+ handle_uuid,
1509
+ len(data),
1510
+ )
1413
1511
  self._classicEstablishedNotifyCallback(handle, data)
1414
1512
  return
1415
1513
  if self._protocolMode == ProtocolMode.CLASSIC:
1514
+ self._logger.debug(
1515
+ "[CASAMBI_NOTIFY_ROUTE] classic_by_mode uuid=%s len=%d",
1516
+ handle_uuid,
1517
+ len(data),
1518
+ )
1416
1519
  self._classicEstablishedNotifyCallback(handle, data)
1417
1520
  return
1418
1521
 
@@ -1632,6 +1735,25 @@ class CasambiClient:
1632
1735
  "payload": payload,
1633
1736
  }
1634
1737
 
1738
+ def _parse_raw(raw_bytes: bytes) -> dict[str, Any] | None:
1739
+ """Parse as raw (unsigned) Classic data.
1740
+
1741
+ Android classic gateway a1.c.V() receives raw bytes with NO CMAC
1742
+ header — byte 0 is the unit_id directly. Adding this as a candidate
1743
+ avoids silently dropping unsigned notifications.
1744
+ """
1745
+ if not raw_bytes:
1746
+ return None
1747
+ return {
1748
+ "mode": "raw",
1749
+ "auth_level": None,
1750
+ "sig_len": 0,
1751
+ "seq": None,
1752
+ "key_name": None,
1753
+ "verified": None, # raw = unverifiable
1754
+ "payload": raw_bytes,
1755
+ }
1756
+
1635
1757
  # Try the currently selected header mode first, then fall back.
1636
1758
  # Some mixed/legacy setups differ between CA52 (legacy) and auth-UUID (conformant).
1637
1759
  parsed_candidates: list[dict[str, Any]] = []
@@ -1653,6 +1775,12 @@ class CasambiClient:
1653
1775
  if r is not None:
1654
1776
  parsed_candidates.append(r)
1655
1777
 
1778
+ # Add a raw (unsigned) candidate — needed because Android classic
1779
+ # gateway receives raw bytes with no CMAC header.
1780
+ raw_candidate = _parse_raw(raw)
1781
+ if raw_candidate is not None:
1782
+ parsed_candidates.append(raw_candidate)
1783
+
1656
1784
  if not parsed_candidates:
1657
1785
  self._classicRxParseFail += 1
1658
1786
  if self._logLimiter.allow("classic_rx_parse_fail", burst=5, window_s=60.0):
@@ -1726,7 +1854,8 @@ class CasambiClient:
1726
1854
  )
1727
1855
 
1728
1856
  # Auto-correct header mode if the other format parses much better.
1729
- if best["mode"] != preferred:
1857
+ # Never switch to "raw" raw is not a header mode, only a fallback parse.
1858
+ if best["mode"] != preferred and best["mode"] in ("conformant", "legacy"):
1730
1859
  # Only switch if we got a stronger signal (verified or plausible payload with fewer assumptions).
1731
1860
  if best["score"] >= 50 and self._logLimiter.allow("classic_rx_mode_switch", burst=3, window_s=3600.0):
1732
1861
  self._logger.warning(
@@ -1928,11 +2057,11 @@ class CasambiClient:
1928
2057
  def _parseClassicUnitStates(self, data: bytes) -> None:
1929
2058
  """Parse Classic unit state records.
1930
2059
 
1931
- Ground truth: casambi-android C1751c.V() (line 301+).
2060
+ Ground truth: casambi-android a1.c.V() (line 226+).
1932
2061
  Format is completely different from EVO _parseUnitStates:
1933
2062
  - flags lower nibble = state_len (EVO uses a separate byte)
1934
- - flags bit 5 = extra1 present, bit 6 = extra2 present, bit 7 = offline
1935
- - unit_id 0xF0 = command response (skip)
2063
+ - flags bit 4 = priority 14, bit 5 = extra1, bit 6 = extra2, bit 7 = online
2064
+ - unit_id 0xF0 = command response (cmd_id + seq + payload)
1936
2065
  """
1937
2066
  self._logger.debug("Parsing Classic unit states...")
1938
2067
  if self._logger.isEnabledFor(logging.DEBUG):
@@ -1942,7 +2071,8 @@ class CasambiClient:
1942
2071
  old_pos = 0
1943
2072
  records_parsed = 0
1944
2073
  try:
1945
- while pos + 2 <= len(data):
2074
+ # Android uses fVar.available() >= 3 as loop guard.
2075
+ while pos + 3 <= len(data):
1946
2076
  unit_id = data[pos]
1947
2077
  flags = data[pos + 1]
1948
2078
  pos += 2
@@ -1950,10 +2080,21 @@ class CasambiClient:
1950
2080
  state_len = flags & 0x0F
1951
2081
  has_extra1 = (flags & 0x20) != 0
1952
2082
  has_extra2 = (flags & 0x40) != 0
1953
- is_offline = (flags & 0x80) != 0
2083
+ # Android a1.c.java:286: (b6 & 128) != 0 → online (NOT offline).
2084
+ # Confirmed by N1.java:1298 log "Set unit ONLINE=" + z6.
2085
+ online = (flags & 0x80) != 0
1954
2086
 
1955
- # 0xF0 = command response record, skip state_len bytes.
2087
+ # 0xF0 = command response record (Android a1.c.java:260-270).
2088
+ # Format: cmd_id(1) + seq(1) + payload(state_len - 2).
1956
2089
  if unit_id == 0xF0:
2090
+ cmd_id = data[pos] if pos < len(data) else None
2091
+ seq_byte = data[pos + 1] if pos + 1 < len(data) else None
2092
+ self._logger.debug(
2093
+ "[CASAMBI_CLASSIC_CMD_RESP] cmd_id=%s seq=%s state_len=%d",
2094
+ cmd_id,
2095
+ seq_byte,
2096
+ state_len,
2097
+ )
1957
2098
  pos += state_len
1958
2099
  continue
1959
2100
 
@@ -1982,17 +2123,15 @@ class CasambiClient:
1982
2123
  if records_parsed <= 10 or self._logger.isEnabledFor(logging.DEBUG):
1983
2124
  self._logger.warning(
1984
2125
  "[CASAMBI_CLASSIC_STATE_PARSED] unit=%d flags=0x%02x state_len=%d "
1985
- "offline=%s extra1=%d extra2=%d state=%s",
2126
+ "online=%s extra1=%d extra2=%d state=%s",
1986
2127
  unit_id,
1987
2128
  flags,
1988
2129
  state_len,
1989
- is_offline,
2130
+ online,
1990
2131
  extra1,
1991
2132
  extra2,
1992
2133
  b2a(state),
1993
2134
  )
1994
-
1995
- online = not is_offline
1996
2135
  # Let Unit.is_on derive actual on/off from state bytes (dimmer, onoff).
1997
2136
  on = True
1998
2137
 
@@ -2004,7 +2143,8 @@ class CasambiClient:
2004
2143
  "on": on,
2005
2144
  "state": state,
2006
2145
  "flags": flags,
2007
- "prio": 0,
2146
+ # Android a1.c.java:291: (b6 & 16) != 0 ? 14 : 0
2147
+ "prio": 14 if (flags & 0x10) else 0,
2008
2148
  "state_len": state_len,
2009
2149
  "padding_len": 0,
2010
2150
  "con": None,
@@ -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.dev14"
10
+ __version__ = "0.3.12.dev15"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: casambi-bt-revamped
3
- Version: 0.3.12.dev14
3
+ Version: 0.3.12.dev15
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