casambi-bt-revamped 0.3.12.dev14__py3-none-any.whl → 0.3.12.dev16__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 +230 -51
- CasambiBt/_version.py +1 -1
- {casambi_bt_revamped-0.3.12.dev14.dist-info → casambi_bt_revamped-0.3.12.dev16.dist-info}/METADATA +1 -1
- {casambi_bt_revamped-0.3.12.dev14.dist-info → casambi_bt_revamped-0.3.12.dev16.dist-info}/RECORD +7 -7
- {casambi_bt_revamped-0.3.12.dev14.dist-info → casambi_bt_revamped-0.3.12.dev16.dist-info}/WHEEL +0 -0
- {casambi_bt_revamped-0.3.12.dev14.dist-info → casambi_bt_revamped-0.3.12.dev16.dist-info}/licenses/LICENSE +0 -0
- {casambi_bt_revamped-0.3.12.dev14.dist-info → casambi_bt_revamped-0.3.12.dev16.dist-info}/top_level.txt +0 -0
CasambiBt/_client.py
CHANGED
|
@@ -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).
|
|
1318
1352
|
|
|
1319
|
-
Ground truth
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
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)
|
|
1359
|
+
|
|
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
|
|
|
@@ -1521,6 +1624,39 @@ class CasambiClient:
|
|
|
1521
1624
|
visitor_key = self._network.classicVisitorKey()
|
|
1522
1625
|
manager_key = self._network.classicManagerKey()
|
|
1523
1626
|
|
|
1627
|
+
def _walk_classic_records(data: bytes) -> bool:
|
|
1628
|
+
"""Check if data is a plausible Classic unit state record stream.
|
|
1629
|
+
|
|
1630
|
+
Walks the same record structure as _parseClassicUnitStates:
|
|
1631
|
+
- unit_id(1) + flags(1) + optional extras + state(state_len)
|
|
1632
|
+
- unit_id 0xF0 is a command response (no extras, state_len bytes consumed)
|
|
1633
|
+
Accepts if >=1 record parsed AND pos ends exactly at len(data).
|
|
1634
|
+
"""
|
|
1635
|
+
pos = 0
|
|
1636
|
+
count = 0
|
|
1637
|
+
while pos + 3 <= len(data): # Match Android available()>=3
|
|
1638
|
+
unit_id = data[pos]
|
|
1639
|
+
flags = data[pos + 1]
|
|
1640
|
+
state_len = flags & 0x0F
|
|
1641
|
+
# 0 and 255 are Classic control bytes, not valid unit records
|
|
1642
|
+
# inside a record stream (handled at dispatch level).
|
|
1643
|
+
if unit_id == 0 or unit_id == 255:
|
|
1644
|
+
return False
|
|
1645
|
+
pos += 2
|
|
1646
|
+
if unit_id == 0xF0:
|
|
1647
|
+
# Command response: cmd_id(1) + seq(1) + payload(state_len-2).
|
|
1648
|
+
if state_len < 2:
|
|
1649
|
+
return False
|
|
1650
|
+
pos += state_len
|
|
1651
|
+
else:
|
|
1652
|
+
has_extra1 = (flags & 0x20) != 0
|
|
1653
|
+
has_extra2 = (flags & 0x40) != 0
|
|
1654
|
+
pos += int(has_extra1) + int(has_extra2) + state_len
|
|
1655
|
+
if pos > len(data):
|
|
1656
|
+
return False
|
|
1657
|
+
count += 1
|
|
1658
|
+
return count >= 1 and pos == len(data)
|
|
1659
|
+
|
|
1524
1660
|
def _plausible_payload(payload: bytes) -> bool:
|
|
1525
1661
|
if not payload:
|
|
1526
1662
|
return False
|
|
@@ -1535,6 +1671,12 @@ class CasambiClient:
|
|
|
1535
1671
|
rec_len = (payload[0] - 239) & 0xFF
|
|
1536
1672
|
if 2 <= rec_len <= len(payload):
|
|
1537
1673
|
return True
|
|
1674
|
+
# Classic unit state record stream or control byte.
|
|
1675
|
+
if self._protocolMode == ProtocolMode.CLASSIC:
|
|
1676
|
+
if payload[0] in (0, 255):
|
|
1677
|
+
return True
|
|
1678
|
+
if _walk_classic_records(payload):
|
|
1679
|
+
return True
|
|
1538
1680
|
return False
|
|
1539
1681
|
|
|
1540
1682
|
def _score(verified: bool | None, payload: bytes) -> int:
|
|
@@ -1632,6 +1774,25 @@ class CasambiClient:
|
|
|
1632
1774
|
"payload": payload,
|
|
1633
1775
|
}
|
|
1634
1776
|
|
|
1777
|
+
def _parse_raw(raw_bytes: bytes) -> dict[str, Any] | None:
|
|
1778
|
+
"""Parse as raw (unsigned) Classic data.
|
|
1779
|
+
|
|
1780
|
+
Android classic gateway a1.c.V() receives raw bytes with NO CMAC
|
|
1781
|
+
header — byte 0 is the unit_id directly. Adding this as a candidate
|
|
1782
|
+
avoids silently dropping unsigned notifications.
|
|
1783
|
+
"""
|
|
1784
|
+
if not raw_bytes:
|
|
1785
|
+
return None
|
|
1786
|
+
return {
|
|
1787
|
+
"mode": "raw",
|
|
1788
|
+
"auth_level": None,
|
|
1789
|
+
"sig_len": 0,
|
|
1790
|
+
"seq": None,
|
|
1791
|
+
"key_name": None,
|
|
1792
|
+
"verified": None, # raw = unverifiable
|
|
1793
|
+
"payload": raw_bytes,
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1635
1796
|
# Try the currently selected header mode first, then fall back.
|
|
1636
1797
|
# Some mixed/legacy setups differ between CA52 (legacy) and auth-UUID (conformant).
|
|
1637
1798
|
parsed_candidates: list[dict[str, Any]] = []
|
|
@@ -1653,6 +1814,12 @@ class CasambiClient:
|
|
|
1653
1814
|
if r is not None:
|
|
1654
1815
|
parsed_candidates.append(r)
|
|
1655
1816
|
|
|
1817
|
+
# Add a raw (unsigned) candidate — needed because Android classic
|
|
1818
|
+
# gateway receives raw bytes with no CMAC header.
|
|
1819
|
+
raw_candidate = _parse_raw(raw)
|
|
1820
|
+
if raw_candidate is not None:
|
|
1821
|
+
parsed_candidates.append(raw_candidate)
|
|
1822
|
+
|
|
1656
1823
|
if not parsed_candidates:
|
|
1657
1824
|
self._classicRxParseFail += 1
|
|
1658
1825
|
if self._logLimiter.allow("classic_rx_parse_fail", burst=5, window_s=60.0):
|
|
@@ -1726,7 +1893,8 @@ class CasambiClient:
|
|
|
1726
1893
|
)
|
|
1727
1894
|
|
|
1728
1895
|
# Auto-correct header mode if the other format parses much better.
|
|
1729
|
-
|
|
1896
|
+
# Never switch to "raw" — raw is not a header mode, only a fallback parse.
|
|
1897
|
+
if best["mode"] != preferred and best["mode"] in ("conformant", "legacy"):
|
|
1730
1898
|
# Only switch if we got a stronger signal (verified or plausible payload with fewer assumptions).
|
|
1731
1899
|
if best["score"] >= 50 and self._logLimiter.allow("classic_rx_mode_switch", burst=3, window_s=3600.0):
|
|
1732
1900
|
self._logger.warning(
|
|
@@ -1928,11 +2096,11 @@ class CasambiClient:
|
|
|
1928
2096
|
def _parseClassicUnitStates(self, data: bytes) -> None:
|
|
1929
2097
|
"""Parse Classic unit state records.
|
|
1930
2098
|
|
|
1931
|
-
Ground truth: casambi-android
|
|
2099
|
+
Ground truth: casambi-android a1.c.V() (line 226+).
|
|
1932
2100
|
Format is completely different from EVO _parseUnitStates:
|
|
1933
2101
|
- flags lower nibble = state_len (EVO uses a separate byte)
|
|
1934
|
-
- flags bit 5 = extra1
|
|
1935
|
-
- unit_id 0xF0 = command response (
|
|
2102
|
+
- flags bit 4 = priority 14, bit 5 = extra1, bit 6 = extra2, bit 7 = online
|
|
2103
|
+
- unit_id 0xF0 = command response (cmd_id + seq + payload)
|
|
1936
2104
|
"""
|
|
1937
2105
|
self._logger.debug("Parsing Classic unit states...")
|
|
1938
2106
|
if self._logger.isEnabledFor(logging.DEBUG):
|
|
@@ -1942,7 +2110,8 @@ class CasambiClient:
|
|
|
1942
2110
|
old_pos = 0
|
|
1943
2111
|
records_parsed = 0
|
|
1944
2112
|
try:
|
|
1945
|
-
|
|
2113
|
+
# Android uses fVar.available() >= 3 as loop guard.
|
|
2114
|
+
while pos + 3 <= len(data):
|
|
1946
2115
|
unit_id = data[pos]
|
|
1947
2116
|
flags = data[pos + 1]
|
|
1948
2117
|
pos += 2
|
|
@@ -1950,10 +2119,21 @@ class CasambiClient:
|
|
|
1950
2119
|
state_len = flags & 0x0F
|
|
1951
2120
|
has_extra1 = (flags & 0x20) != 0
|
|
1952
2121
|
has_extra2 = (flags & 0x40) != 0
|
|
1953
|
-
|
|
2122
|
+
# Android a1.c.java:286: (b6 & 128) != 0 → online (NOT offline).
|
|
2123
|
+
# Confirmed by N1.java:1298 log "Set unit ONLINE=" + z6.
|
|
2124
|
+
online = (flags & 0x80) != 0
|
|
1954
2125
|
|
|
1955
|
-
# 0xF0 = command response record
|
|
2126
|
+
# 0xF0 = command response record (Android a1.c.java:260-270).
|
|
2127
|
+
# Format: cmd_id(1) + seq(1) + payload(state_len - 2).
|
|
1956
2128
|
if unit_id == 0xF0:
|
|
2129
|
+
cmd_id = data[pos] if pos < len(data) else None
|
|
2130
|
+
seq_byte = data[pos + 1] if pos + 1 < len(data) else None
|
|
2131
|
+
self._logger.debug(
|
|
2132
|
+
"[CASAMBI_CLASSIC_CMD_RESP] cmd_id=%s seq=%s state_len=%d",
|
|
2133
|
+
cmd_id,
|
|
2134
|
+
seq_byte,
|
|
2135
|
+
state_len,
|
|
2136
|
+
)
|
|
1957
2137
|
pos += state_len
|
|
1958
2138
|
continue
|
|
1959
2139
|
|
|
@@ -1982,17 +2162,15 @@ class CasambiClient:
|
|
|
1982
2162
|
if records_parsed <= 10 or self._logger.isEnabledFor(logging.DEBUG):
|
|
1983
2163
|
self._logger.warning(
|
|
1984
2164
|
"[CASAMBI_CLASSIC_STATE_PARSED] unit=%d flags=0x%02x state_len=%d "
|
|
1985
|
-
"
|
|
2165
|
+
"online=%s extra1=%d extra2=%d state=%s",
|
|
1986
2166
|
unit_id,
|
|
1987
2167
|
flags,
|
|
1988
2168
|
state_len,
|
|
1989
|
-
|
|
2169
|
+
online,
|
|
1990
2170
|
extra1,
|
|
1991
2171
|
extra2,
|
|
1992
2172
|
b2a(state),
|
|
1993
2173
|
)
|
|
1994
|
-
|
|
1995
|
-
online = not is_offline
|
|
1996
2174
|
# Let Unit.is_on derive actual on/off from state bytes (dimmer, onoff).
|
|
1997
2175
|
on = True
|
|
1998
2176
|
|
|
@@ -2004,7 +2182,8 @@ class CasambiClient:
|
|
|
2004
2182
|
"on": on,
|
|
2005
2183
|
"state": state,
|
|
2006
2184
|
"flags": flags,
|
|
2007
|
-
|
|
2185
|
+
# Android a1.c.java:291: (b6 & 16) != 0 ? 14 : 0
|
|
2186
|
+
"prio": 14 if (flags & 0x10) else 0,
|
|
2008
2187
|
"state_len": state_len,
|
|
2009
2188
|
"padding_len": 0,
|
|
2010
2189
|
"con": None,
|
CasambiBt/_version.py
CHANGED
{casambi_bt_revamped-0.3.12.dev14.dist-info → casambi_bt_revamped-0.3.12.dev16.dist-info}/METADATA
RENAMED
|
@@ -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.dev16
|
|
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
|
{casambi_bt_revamped-0.3.12.dev14.dist-info → casambi_bt_revamped-0.3.12.dev16.dist-info}/RECORD
RENAMED
|
@@ -2,7 +2,7 @@ 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=
|
|
5
|
+
CasambiBt/_client.py,sha256=uieXtm5hRRMCUyxhsY1ZwSFs1mVvBJbe3JMkWQsayGk,105829
|
|
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
|
|
@@ -12,11 +12,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=
|
|
15
|
+
CasambiBt/_version.py,sha256=gV1qUyxx1Ng85vM0VQRn42juijLHgX2A69jBFD8mmfQ,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.
|
|
19
|
-
casambi_bt_revamped-0.3.12.
|
|
20
|
-
casambi_bt_revamped-0.3.12.
|
|
21
|
-
casambi_bt_revamped-0.3.12.
|
|
22
|
-
casambi_bt_revamped-0.3.12.
|
|
18
|
+
casambi_bt_revamped-0.3.12.dev16.dist-info/licenses/LICENSE,sha256=TAIIitFxpxEDi6Iju7foW4TDQmWvC-IhLVLhl67jKmQ,11341
|
|
19
|
+
casambi_bt_revamped-0.3.12.dev16.dist-info/METADATA,sha256=p35CV0zqdNSbxrs0JzqVKlbOnS0F5aU2Flcty5SEw1k,5878
|
|
20
|
+
casambi_bt_revamped-0.3.12.dev16.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
21
|
+
casambi_bt_revamped-0.3.12.dev16.dist-info/top_level.txt,sha256=uNbqLjtecFosoFzpGAC89-5icikWODKI8rOjbi8v_sA,10
|
|
22
|
+
casambi_bt_revamped-0.3.12.dev16.dist-info/RECORD,,
|
{casambi_bt_revamped-0.3.12.dev14.dist-info → casambi_bt_revamped-0.3.12.dev16.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|