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.
- {casambi_bt_revamped-0.3.12.dev14/src/casambi_bt_revamped.egg-info → casambi_bt_revamped-0.3.12.dev15}/PKG-INFO +1 -1
- {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/setup.cfg +1 -1
- {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_client.py +191 -51
- {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_version.py +1 -1
- {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15/src/casambi_bt_revamped.egg-info}/PKG-INFO +1 -1
- {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/LICENSE +0 -0
- {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/README.md +0 -0
- {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/pyproject.toml +0 -0
- {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/__init__.py +0 -0
- {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_cache.py +0 -0
- {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_casambi.py +0 -0
- {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_classic_crypto.py +0 -0
- {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_constants.py +0 -0
- {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_discover.py +0 -0
- {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_encryption.py +0 -0
- {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_invocation.py +0 -0
- {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_keystore.py +0 -0
- {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_network.py +0 -0
- {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_operation.py +0 -0
- {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_switch_events.py +0 -0
- {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_unit.py +0 -0
- {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/errors.py +0 -0
- {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/py.typed +0 -0
- {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/casambi_bt_revamped.egg-info/SOURCES.txt +0 -0
- {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
- {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/casambi_bt_revamped.egg-info/requires.txt +0 -0
- {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
- {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/tests/test_classic_protocol.py +0 -0
- {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/tests/test_legacy_protocol_handling.py +0 -0
- {casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/tests/test_switch_event_logs.py +0 -0
- {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.
|
|
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.
|
|
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
|
{casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_client.py
RENAMED
|
@@ -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(
|
|
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
|
-
#
|
|
1126
|
-
#
|
|
1127
|
-
#
|
|
1128
|
-
#
|
|
1129
|
-
#
|
|
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
|
|
1132
|
-
key = None
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
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
|
-
#
|
|
1222
|
-
#
|
|
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=
|
|
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
|
-
|
|
1320
|
-
|
|
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:
|
|
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
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
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((
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
1935
|
-
- unit_id 0xF0 = command response (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
"
|
|
2126
|
+
"online=%s extra1=%d extra2=%d state=%s",
|
|
1986
2127
|
unit_id,
|
|
1987
2128
|
flags,
|
|
1988
2129
|
state_len,
|
|
1989
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: casambi-bt-revamped
|
|
3
|
-
Version: 0.3.12.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/__init__.py
RENAMED
|
File without changes
|
{casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_cache.py
RENAMED
|
File without changes
|
{casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_casambi.py
RENAMED
|
File without changes
|
|
File without changes
|
{casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_constants.py
RENAMED
|
File without changes
|
{casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_discover.py
RENAMED
|
File without changes
|
{casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_encryption.py
RENAMED
|
File without changes
|
{casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_invocation.py
RENAMED
|
File without changes
|
{casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_keystore.py
RENAMED
|
File without changes
|
{casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_network.py
RENAMED
|
File without changes
|
{casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_operation.py
RENAMED
|
File without changes
|
|
File without changes
|
{casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/_unit.py
RENAMED
|
File without changes
|
{casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/errors.py
RENAMED
|
File without changes
|
{casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/src/CasambiBt/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/tests/test_classic_protocol.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{casambi_bt_revamped-0.3.12.dev14 → casambi_bt_revamped-0.3.12.dev15}/tests/test_unit_state_logs.py
RENAMED
|
File without changes
|